From 1a1d068388c1f71be0f6cd4d1c7300e9805c71af Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 13:12:18 +0000 Subject: [PATCH 01/14] feat: add built-in prompts and prompt templates for agents (#1401) Add Go text/template support to the Agent CRD's systemMessage field, enabling composable prompt fragments and variable interpolation. Templates are resolved at reconciliation time by the controller. - Add PromptSource type using inlined TypedLocalReference for extensibility - Add promptSources field to DeclarativeAgentSpec - Implement template resolution with {{include "source/key"}} syntax and agent context variables (AgentName, AgentNamespace, Description, ToolNames, SkillNames) - Add ConfigMap watch to agent controller for automatic re-reconciliation - Ship kagent-builtin-prompts Helm ConfigMap with 5 built-in templates (skills-usage, tool-usage-best-practices, safety-guardrails, kubernetes-context, a2a-communication) - Add GetConfigMapData/GetSecretData utility functions - Add architecture documentation in docs/architecture/prompt-templates.md Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- docs/architecture/prompt-templates.md | 285 ++++++++++++++++++ go/api/v1alpha2/agent_types.go | 29 +- go/api/v1alpha2/zz_generated.deepcopy.go | 21 ++ go/internal/controller/agent_controller.go | 61 ++++ .../translator/agent/adk_api_translator.go | 32 +- .../controller/translator/agent/template.go | 140 +++++++++ .../translator/agent/template_test.go | 276 +++++++++++++++++ go/internal/utils/config_map.go | 9 + go/internal/utils/secret.go | 13 + .../templates/builtin-prompts-configmap.yaml | 85 ++++++ 10 files changed, 946 insertions(+), 5 deletions(-) create mode 100644 docs/architecture/prompt-templates.md create mode 100644 go/internal/controller/translator/agent/template.go create mode 100644 go/internal/controller/translator/agent/template_test.go create mode 100644 helm/kagent/templates/builtin-prompts-configmap.yaml diff --git a/docs/architecture/prompt-templates.md b/docs/architecture/prompt-templates.md new file mode 100644 index 000000000..20a187c19 --- /dev/null +++ b/docs/architecture/prompt-templates.md @@ -0,0 +1,285 @@ +# Prompt Templates + +This document explains the prompt templates feature — how it works, the design decisions behind it, and how to use it. + +## Overview + +Prompt templates allow agents to compose their system messages from reusable fragments and dynamic variables, instead of writing everything from scratch. The system message field on the Agent CRD becomes a Go [text/template](https://pkg.go.dev/text/template) when prompt sources are configured, supporting two capabilities: + +1. **Include directives** — `{{include "source/key"}}` inserts a prompt fragment from a ConfigMap or Secret +2. **Variable interpolation** — `{{.AgentName}}`, `{{.ToolNames}}`, etc. inject agent metadata + +Templates are resolved at **reconciliation time** by the controller. The final, fully-resolved prompt is baked into the agent's config Secret, so the Python ADK runtime receives a plain string with no template syntax. + +```text +┌──────────────────────────────────────────────────────────┐ +│ Agent CRD │ +│ │ +│ systemMessage: | │ +│ {{include "builtin/skills-usage"}} │ +│ You are {{.AgentName}}. │ +│ │ +│ promptSources: │ +│ - kind: ConfigMap │ +│ name: kagent-builtin-prompts │ +│ alias: builtin │ +└──────────────────────┬───────────────────────────────────┘ + │ reconciliation + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Controller (Go) │ +│ │ +│ 1. Resolve raw system message │ +│ 2. Fetch all data from referenced ConfigMaps/Secrets │ +│ 3. Build lookup map: "alias/key" → value │ +│ 4. Build template context from agent metadata │ +│ 5. Execute Go text/template with include + variables │ +│ 6. Store resolved string in config Secret │ +└──────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Config Secret → Python ADK │ +│ │ +│ instruction: | │ +│ ## Skills │ +│ You have access to skills — pre-built capabilities... │ +│ You are my-agent. │ +└──────────────────────────────────────────────────────────┘ +``` + +## Design Decisions + +### Why `source/key` syntax? + +ConfigMaps naturally contain multiple keys. Rather than requiring users to declare each key individually as a separate template reference, the `source/key` syntax lets you reference an entire ConfigMap once and access any of its keys directly. This significantly reduces boilerplate when using multiple prompts from the same source. + +The `/` separator (rather than `.`) avoids ambiguity since dots are valid in Kubernetes resource names but slashes are not. + +### Why aliases? + +ConfigMap names can be long (e.g., `kagent-builtin-prompts`). The optional `alias` field lets users define a shorter identifier for use in include directives, keeping the system message readable: `{{include "builtin/skills-usage"}}` instead of `{{include "kagent-builtin-prompts/skills-usage"}}`. + +### Why Go text/template? + +Prompt engineering is a text-authoring activity. Users need fine-grained control over where fragments are placed within their prose — a structured ordered-parts list would force artificial boundaries. Since the controller is written in Go and already uses `text/template` for other purposes (skills init scripts), it was a natural choice. + +### Why resolve at reconciliation time? + +Resolving templates in the controller (rather than at runtime in the Python ADK) gives several benefits: + +- **Predictability** — the resolved prompt is visible in the config Secret, making debugging straightforward +- **No Python-side changes** — the ADK runtime receives a plain string, keeping the runtime simple +- **Validation** — template errors are caught immediately at reconciliation time and surfaced via the Agent's `Accepted` status condition + +### Why no nested includes? + +Content pulled from ConfigMaps via `{{include "source/key"}}` is treated as **plain text**, not as a template. This means included fragments cannot themselves use `{{include}}` or `{{.Variable}}` directives. This keeps the system simple, predictable, and avoids potential circular reference issues. + +### Why `TypedLocalReference` for prompt sources? + +Each prompt source uses an inlined `TypedLocalReference` (`kind`, `apiGroup`, `name`) rather than a fixed enum like `type: ConfigMap`. This makes the API extensible — today it supports ConfigMaps and Secrets (core API group), but a future `PromptLibrary` CRD (`kind: PromptLibrary, apiGroup: kagent.dev`) could be added without changing the schema. + +### Why ConfigMaps as the initial implementation? + +ConfigMaps are a well-understood Kubernetes primitive and sufficient for the initial implementation. The `TypedLocalReference` pattern ensures the API is ready to support custom CRDs in the future without breaking changes. + +### Backwards compatibility + +To avoid breaking existing agents that may have literal `{{` characters in their system messages, the `systemMessage` field is **only** treated as a template when `promptSources` is non-empty. If no prompt sources are configured, the system message is passed through as a plain string, preserving existing behavior. + +## Usage + +### Basic example + +Create a ConfigMap with your prompt fragments, then reference it from your agent: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-prompts + namespace: default +data: + preamble: | + You are a helpful assistant specialized in Kubernetes operations. + Always explain your reasoning before taking action. + safety: | + Never delete resources without explicit user confirmation. + Never expose secrets or credentials in your responses. +--- +apiVersion: kagent.dev/v1alpha2 +kind: Agent +metadata: + name: k8s-helper + namespace: default +spec: + description: "Kubernetes troubleshooting agent" + declarative: + modelConfig: default-model-config + systemMessage: | + {{include "my-prompts/preamble"}} + + Your name is {{.AgentName}} and you operate in the {{.AgentNamespace}} namespace. + Your purpose: {{.Description}} + + {{include "my-prompts/safety"}} + promptSources: + - kind: ConfigMap + name: my-prompts + tools: + - type: McpServer + mcpServer: + name: k8s-tools + kind: RemoteMCPServer + apiGroup: kagent.dev + toolNames: ["get-pods", "describe-pod"] +``` + +This resolves to: + +``` +You are a helpful assistant specialized in Kubernetes operations. +Always explain your reasoning before taking action. + +Your name is k8s-helper and you operate in the default namespace. +Your purpose: Kubernetes troubleshooting agent + +Never delete resources without explicit user confirmation. +Never expose secrets or credentials in your responses. +``` + +### Using aliases + +Use the `alias` field to shorten include paths: + +```yaml +promptSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin + - kind: ConfigMap + name: my-custom-prompts + alias: custom +``` + +Then in your system message: + +```yaml +systemMessage: | + {{include "builtin/skills-usage"}} + {{include "custom/my-rules"}} +``` + +Without aliases, you'd write `{{include "kagent-builtin-prompts/skills-usage"}}`. + +### Using built-in prompts + +Kagent ships a `kagent-builtin-prompts` ConfigMap (deployed via Helm) with the following keys: + +| Key | Description | +|-----|-------------| +| `skills-usage` | Instructions for agents that use skills from `/skills` | +| `tool-usage-best-practices` | Guidelines for effective tool usage | +| `safety-guardrails` | Safety rules for destructive operations and sensitive data | +| `kubernetes-context` | Context about operating within a Kubernetes cluster | +| `a2a-communication` | Guidelines for agent-to-agent communication | + +Example using built-in prompts: + +```yaml +apiVersion: kagent.dev/v1alpha2 +kind: Agent +metadata: + name: my-agent + namespace: kagent # same namespace where kagent is installed +spec: + description: "A safe Kubernetes agent with skills" + declarative: + systemMessage: | + {{include "builtin/skills-usage"}} + {{include "builtin/tool-usage-best-practices"}} + + You are {{.AgentName}}. {{.Description}}. + + {{include "builtin/safety-guardrails"}} + {{include "builtin/kubernetes-context"}} + promptSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin +``` + +### Using multiple sources + +You can reference multiple ConfigMaps and Secrets simultaneously: + +```yaml +promptSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin + - kind: ConfigMap + name: team-prompts + alias: team + - kind: Secret + name: proprietary-prompts + alias: proprietary +``` + +Then use any key from any source: + +```yaml +systemMessage: | + {{include "builtin/safety-guardrails"}} + {{include "team/coding-standards"}} + {{include "proprietary/company-instructions"}} +``` + +### Available template variables + +When `promptSources` is non-empty, the following variables are available in `systemMessage`: + +| Variable | Type | Source | +|----------|------|--------| +| `{{.AgentName}}` | `string` | `metadata.name` | +| `{{.AgentNamespace}}` | `string` | `metadata.namespace` | +| `{{.Description}}` | `string` | `spec.description` | +| `{{.ToolNames}}` | `[]string` | Collected from all `tools[*].mcpServer.toolNames` | +| `{{.SkillNames}}` | `[]string` | Derived from `skills.refs` (OCI image name) and `skills.gitRefs[*].name` | + +You can use standard Go template constructs to work with these variables: + +```yaml +# List tools +Available tools: {{range .ToolNames}}- {{.}} +{{end}} + +# Conditional +{{if .SkillNames}}You have skills available.{{end}} + +# Count +You have {{len .ToolNames}} tools configured. +``` + +## ConfigMap change detection + +The agent controller watches ConfigMaps referenced by agents via `promptSources` or `systemMessageFrom`. When a ConfigMap's content changes, all agents referencing it are automatically re-reconciled, and their resolved system messages are updated. + +## Error handling + +If template resolution fails (missing ConfigMap, invalid template syntax, unknown path in `{{include}}`), the agent's `Accepted` status condition is set to `False` with reason `ReconcileFailed` and a message describing the error. The agent's deployment is not updated until the error is resolved. + +```bash +# Check for template errors +kubectl get agent my-agent -o jsonpath='{.status.conditions[?(@.type=="Accepted")]}' +``` + +## Related Files + +- [agent_types.go](../../go/api/v1alpha2/agent_types.go) — `PromptSource` type and `PromptSources` field +- [template.go](../../go/internal/controller/translator/agent/template.go) — Template resolution logic +- [template_test.go](../../go/internal/controller/translator/agent/template_test.go) — Unit tests +- [adk_api_translator.go](../../go/internal/controller/translator/agent/adk_api_translator.go) — `resolveSystemMessage()` integration +- [agent_controller.go](../../go/internal/controller/agent_controller.go) — ConfigMap watch setup +- [builtin-prompts-configmap.yaml](../../helm/kagent/templates/builtin-prompts-configmap.yaml) — Built-in prompt templates diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index e8725517b..844467bdd 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -118,12 +118,23 @@ type GitRepo struct { // +kubebuilder:validation:XValidation:rule="!has(self.systemMessage) || !has(self.systemMessageFrom)",message="systemMessage and systemMessageFrom are mutually exclusive" type DeclarativeAgentSpec struct { - // SystemMessage is a string specifying the system message for the agent + // SystemMessage is a string specifying the system message for the agent. + // When PromptSources is non-empty, this field is treated as a Go text/template + // with access to an {{include "source/key"}} function and agent context variables + // such as {{.AgentName}}, {{.AgentNamespace}}, {{.Description}}, {{.ToolNames}}, and {{.SkillNames}}. // +optional SystemMessage string `json:"systemMessage,omitempty"` // SystemMessageFrom is a reference to a ConfigMap or Secret containing the system message. + // When PromptSources is non-empty, the resolved value is treated as a Go text/template. // +optional SystemMessageFrom *ValueSource `json:"systemMessageFrom,omitempty"` + // PromptSources defines ConfigMaps or Secrets whose keys can be included in the systemMessage + // using Go template syntax: {{include "alias/key"}} or {{include "name/key"}}. + // When this field is non-empty, systemMessage is treated as a Go template with access to + // the include function and agent context variables. + // +optional + // +kubebuilder:validation:MaxItems=20 + PromptSources []PromptSource `json:"promptSources,omitempty"` // The name of the model config to use. // If not specified, the default value is "default-model-config". // Must be in the same namespace as the Agent. @@ -159,6 +170,22 @@ type DeclarativeAgentSpec struct { Memory *MemorySpec `json:"memory,omitempty"` } +// PromptSource references a Kubernetes resource whose keys are available as prompt fragments. +// Currently supports ConfigMap and Secret resources (core API group). +// In systemMessage templates, use {{include "alias/key"}} (or {{include "name/key"}} if no alias is set) +// to insert the value of a specific key from this source. +type PromptSource struct { + // Inline reference to the Kubernetes resource. + // For ConfigMaps: kind=ConfigMap, apiGroup="" (empty for core API group). + // For Secrets: kind=Secret, apiGroup="" (empty for core API group). + TypedLocalReference `json:",inline"` + + // Alias is an optional short identifier for use in include directives. + // If set, use {{include "alias/key"}} instead of {{include "name/key"}}. + // +optional + Alias string `json:"alias,omitempty"` +} + // MemorySpec enables long-term memory for an agent. type MemorySpec struct { // ModelConfig is the name of the ModelConfig object whose embedding diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 18e00260a..b6439c0fa 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -364,6 +364,11 @@ func (in *DeclarativeAgentSpec) DeepCopyInto(out *DeclarativeAgentSpec) { *out = new(ValueSource) **out = **in } + if in.PromptSources != nil { + in, out := &in.PromptSources, &out.PromptSources + *out = make([]PromptSource, len(*in)) + copy(*out, *in) + } if in.Tools != nil { in, out := &in.Tools, &out.Tools *out = make([]*Tool, len(*in)) @@ -840,6 +845,22 @@ func (in *OpenAIConfig) DeepCopy() *OpenAIConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptSource) DeepCopyInto(out *PromptSource) { + *out = *in + out.TypedLocalReference = in.TypedLocalReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptSource. +func (in *PromptSource) DeepCopy() *PromptSource { + if in == nil { + return nil + } + out := new(PromptSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteMCPServer) DeepCopyInto(out *RemoteMCPServer) { *out = *in diff --git a/go/internal/controller/agent_controller.go b/go/internal/controller/agent_controller.go index e188058f4..d943ca963 100644 --- a/go/internal/controller/agent_controller.go +++ b/go/internal/controller/agent_controller.go @@ -142,6 +142,30 @@ func (r *AgentController) SetupWithManager(mgr ctrl.Manager) error { builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ) + // Watch ConfigMaps referenced by agents via promptTemplates or systemMessageFrom. + // When a ConfigMap changes, re-reconcile any agents that reference it. + build = build.Watches( + &corev1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + requests := []reconcile.Request{} + + for _, agent := range r.findAgentsReferencingConfigMap(ctx, mgr.GetClient(), types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }) { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: agent.Name, + Namespace: agent.Namespace, + }, + }) + } + + return requests + }), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ) + if _, err := mgr.GetRESTMapper().RESTMapping(mcpServerGK); err == nil { build = build.Watches( &v1alpha1.MCPServer{}, @@ -308,6 +332,43 @@ func (r *AgentController) findAgentsUsingModelConfig(ctx context.Context, cl cli return agents } +func (r *AgentController) findAgentsReferencingConfigMap(ctx context.Context, cl client.Client, obj types.NamespacedName) []*v1alpha2.Agent { + var agentsList v1alpha2.AgentList + if err := cl.List(ctx, &agentsList); err != nil { + agentControllerLog.Error(err, "failed to list agents in order to reconcile ConfigMap update") + return nil + } + + var agents []*v1alpha2.Agent + for i := range agentsList.Items { + agent := &agentsList.Items[i] + if agent.Namespace != obj.Namespace { + continue + } + if agent.Spec.Type != v1alpha2.AgentType_Declarative || agent.Spec.Declarative == nil { + continue + } + + // Check if systemMessageFrom references this ConfigMap. + if ref := agent.Spec.Declarative.SystemMessageFrom; ref != nil { + if ref.Type == v1alpha2.ConfigMapValueSource && ref.Name == obj.Name { + agents = append(agents, agent) + continue + } + } + + // Check if any promptSources reference this ConfigMap. + for _, ps := range agent.Spec.Declarative.PromptSources { + if ps.Kind == "ConfigMap" && ps.Name == obj.Name { + agents = append(agents, agent) + break + } + } + } + + return agents +} + type ownedObjectPredicate = typedOwnedObjectPredicate[client.Object] type typedOwnedObjectPredicate[object metav1.Object] struct { diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index 9b589d416..51e384b39 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -667,13 +667,37 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent *v1al } func (a *adkApiTranslator) resolveSystemMessage(ctx context.Context, agent *v1alpha2.Agent) (string, error) { + var rawMessage string if agent.Spec.Declarative.SystemMessageFrom != nil { - return agent.Spec.Declarative.SystemMessageFrom.Resolve(ctx, a.kube, agent.Namespace) + msg, err := agent.Spec.Declarative.SystemMessageFrom.Resolve(ctx, a.kube, agent.Namespace) + if err != nil { + return "", err + } + rawMessage = msg + } else if agent.Spec.Declarative.SystemMessage != "" { + rawMessage = agent.Spec.Declarative.SystemMessage + } else { + return "", fmt.Errorf("at least one system message source (SystemMessage or SystemMessageFrom) must be specified") + } + + // Only treat as a template when promptSources is configured (backwards compatible). + if len(agent.Spec.Declarative.PromptSources) == 0 { + return rawMessage, nil } - if agent.Spec.Declarative.SystemMessage != "" { - return agent.Spec.Declarative.SystemMessage, nil + + lookup, err := resolvePromptSources(ctx, a.kube, agent.Namespace, agent.Spec.Declarative.PromptSources) + if err != nil { + return "", fmt.Errorf("failed to resolve prompt sources: %w", err) } - return "", fmt.Errorf("at least one system message source (SystemMessage or SystemMessageFrom) must be specified") + + tplCtx := buildTemplateContext(agent) + + resolved, err := executeSystemMessageTemplate(rawMessage, lookup, tplCtx) + if err != nil { + return "", fmt.Errorf("failed to execute system message template: %w", err) + } + + return resolved, nil } const ( diff --git a/go/internal/controller/translator/agent/template.go b/go/internal/controller/translator/agent/template.go new file mode 100644 index 000000000..4aed218bb --- /dev/null +++ b/go/internal/controller/translator/agent/template.go @@ -0,0 +1,140 @@ +package agent + +import ( + "bytes" + "context" + "fmt" + "sort" + "strings" + "text/template" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/utils" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// PromptTemplateContext holds the variables available to system message templates. +type PromptTemplateContext struct { + // AgentName is the metadata.name of the Agent resource. + AgentName string + // AgentNamespace is the metadata.namespace of the Agent resource. + AgentNamespace string + // Description is the spec.description of the Agent resource. + Description string + // ToolNames is the list of tool names from all MCP server tools configured on the agent. + ToolNames []string + // SkillNames is the list of skill identifiers configured on the agent. + SkillNames []string +} + +// resolvePromptSources fetches all data from the referenced ConfigMaps/Secrets and builds +// a lookup map keyed by "identifier/key" where identifier is the alias (if set) or resource name. +func resolvePromptSources(ctx context.Context, kube client.Client, namespace string, sources []v1alpha2.PromptSource) (map[string]string, error) { + lookup := make(map[string]string) + + for _, src := range sources { + identifier := src.Name + if src.Alias != "" { + identifier = src.Alias + } + + nn := src.NamespacedName(namespace) + + var data map[string]string + var err error + + switch src.Kind { + case "ConfigMap": + data, err = utils.GetConfigMapData(ctx, kube, nn) + case "Secret": + data, err = utils.GetSecretData(ctx, kube, nn) + default: + return nil, fmt.Errorf("unsupported prompt source kind %q (apiGroup=%q) for %q", src.Kind, src.ApiGroup, src.Name) + } + if err != nil { + return nil, fmt.Errorf("failed to resolve prompt source %q: %w", src.Name, err) + } + + for key, value := range data { + lookupKey := identifier + "/" + key + lookup[lookupKey] = value + } + } + + return lookup, nil +} + +// buildTemplateContext constructs the template context from an Agent resource. +func buildTemplateContext(agent *v1alpha2.Agent) PromptTemplateContext { + ctx := PromptTemplateContext{ + AgentName: agent.Name, + AgentNamespace: agent.Namespace, + Description: agent.Spec.Description, + } + + // Collect tool names from all MCP server tools. + if agent.Spec.Declarative != nil { + for _, tool := range agent.Spec.Declarative.Tools { + if tool.McpServer != nil { + ctx.ToolNames = append(ctx.ToolNames, tool.McpServer.ToolNames...) + } + } + } + + // Collect skill names from OCI refs and git refs. + if agent.Spec.Skills != nil { + for _, ref := range agent.Spec.Skills.Refs { + // Use the last segment of the OCI reference as the skill name. + parts := strings.Split(ref, "/") + name := parts[len(parts)-1] + // Strip tag if present (e.g., "image:v1" -> "image"). + if idx := strings.Index(name, ":"); idx != -1 { + name = name[:idx] + } + ctx.SkillNames = append(ctx.SkillNames, name) + } + for _, gitRef := range agent.Spec.Skills.GitRefs { + if gitRef.Name != "" { + ctx.SkillNames = append(ctx.SkillNames, gitRef.Name) + } else { + // Fall back to repo URL last segment. + parts := strings.Split(strings.TrimSuffix(gitRef.URL, ".git"), "/") + ctx.SkillNames = append(ctx.SkillNames, parts[len(parts)-1]) + } + } + } + + return ctx +} + +// executeSystemMessageTemplate parses and executes the system message as a Go text/template. +// The include function resolves "source/key" paths from the provided lookup map. +// Included content is treated as plain text (no nested template execution). +func executeSystemMessageTemplate(rawMessage string, lookup map[string]string, tplCtx PromptTemplateContext) (string, error) { + funcMap := template.FuncMap{ + "include": func(path string) (string, error) { + content, ok := lookup[path] + if !ok { + available := make([]string, 0, len(lookup)) + for k := range lookup { + available = append(available, k) + } + sort.Strings(available) + return "", fmt.Errorf("prompt template %q not found in promptSources, available: %v", path, available) + } + return content, nil + }, + } + + tmpl, err := template.New("systemMessage").Funcs(funcMap).Parse(rawMessage) + if err != nil { + return "", fmt.Errorf("failed to parse system message template: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, tplCtx); err != nil { + return "", fmt.Errorf("failed to execute system message template: %w", err) + } + + return buf.String(), nil +} diff --git a/go/internal/controller/translator/agent/template_test.go b/go/internal/controller/translator/agent/template_test.go new file mode 100644 index 000000000..ef629fc97 --- /dev/null +++ b/go/internal/controller/translator/agent/template_test.go @@ -0,0 +1,276 @@ +package agent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" +) + +func TestExecuteSystemMessageTemplate(t *testing.T) { + tests := []struct { + name string + raw string + lookup map[string]string + ctx PromptTemplateContext + want string + wantErr bool + errMsg string + }{ + { + name: "include single key from source", + raw: `{{include "prompts/greeting"}} Have a great day.`, + lookup: map[string]string{"prompts/greeting": "Hello, world!"}, + ctx: PromptTemplateContext{}, + want: "Hello, world! Have a great day.", + }, + { + name: "include multiple keys from same source", + raw: `{{include "prompts/header"}} + +You are a custom agent. + +{{include "prompts/footer"}}`, + lookup: map[string]string{ + "prompts/header": "# System Instructions", + "prompts/footer": "# End of Instructions", + }, + ctx: PromptTemplateContext{}, + want: `# System Instructions + +You are a custom agent. + +# End of Instructions`, + }, + { + name: "include keys from multiple sources", + raw: `{{include "builtin/safety"}} {{include "custom/rules"}}`, + lookup: map[string]string{ + "builtin/safety": "Be safe.", + "custom/rules": "Follow the rules.", + }, + ctx: PromptTemplateContext{}, + want: "Be safe. Follow the rules.", + }, + { + name: "variable interpolation - AgentName", + raw: `You are {{.AgentName}}, operating in {{.AgentNamespace}}.`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{ + AgentName: "my-agent", + AgentNamespace: "production", + }, + want: "You are my-agent, operating in production.", + }, + { + name: "variable interpolation - Description", + raw: `Agent purpose: {{.Description}}`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{ + Description: "A Kubernetes troubleshooting agent", + }, + want: "Agent purpose: A Kubernetes troubleshooting agent", + }, + { + name: "variable interpolation - ToolNames with range", + raw: `Tools: {{range .ToolNames}}{{.}}, {{end}}`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{ + ToolNames: []string{"get-pods", "describe-pod", "get-logs"}, + }, + want: "Tools: get-pods, describe-pod, get-logs, ", + }, + { + name: "variable interpolation - SkillNames", + raw: `Skills: {{range .SkillNames}}{{.}}, {{end}}`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{ + SkillNames: []string{"k8s-debug", "helm-deploy"}, + }, + want: "Skills: k8s-debug, helm-deploy, ", + }, + { + name: "combined include and interpolation", + raw: `{{include "builtin/skills-usage"}} + +You are {{.AgentName}} ({{.Description}}). +Available tools: {{range .ToolNames}}{{.}}, {{end}}`, + lookup: map[string]string{ + "builtin/skills-usage": "## Skills\nUse skills from /skills directory.", + }, + ctx: PromptTemplateContext{ + AgentName: "k8s-agent", + Description: "Kubernetes helper", + ToolNames: []string{"get-pods", "apply-manifest"}, + }, + want: `## Skills +Use skills from /skills directory. + +You are k8s-agent (Kubernetes helper). +Available tools: get-pods, apply-manifest, `, + }, + { + name: "missing key in source", + raw: `{{include "prompts/nonexistent"}}`, + lookup: map[string]string{"prompts/other": "content"}, + ctx: PromptTemplateContext{}, + wantErr: true, + errMsg: "nonexistent", + }, + { + name: "invalid template syntax", + raw: `{{invalid syntax`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{}, + wantErr: true, + errMsg: "failed to parse", + }, + { + name: "no nested template execution in included content", + raw: `{{include "prompts/tpl"}}`, + lookup: map[string]string{"prompts/tpl": `This has {{.AgentName}} but should be literal`}, + ctx: PromptTemplateContext{ + AgentName: "should-not-appear", + }, + // Included content is plain text, so {{.AgentName}} remains literal. + want: `This has {{.AgentName}} but should be literal`, + }, + { + name: "empty lookup map with no directives", + raw: `Plain system message with no templates.`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{}, + want: "Plain system message with no templates.", + }, + { + name: "empty ToolNames and SkillNames", + raw: `Agent: {{.AgentName}}, tools: {{len .ToolNames}}, skills: {{len .SkillNames}}`, + lookup: map[string]string{}, + ctx: PromptTemplateContext{ + AgentName: "test", + }, + want: "Agent: test, tools: 0, skills: 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := executeSystemMessageTemplate(tt.raw, tt.lookup, tt.ctx) + if tt.wantErr { + require.Error(t, err) + if tt.errMsg != "" { + assert.Contains(t, err.Error(), tt.errMsg) + } + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestBuildTemplateContext(t *testing.T) { + tests := []struct { + name string + agent *v1alpha2.Agent + wantCtx PromptTemplateContext + }{ + { + name: "full agent with tools and skills", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-agent", + Namespace: "production", + }, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "A helpful agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Tools: []*v1alpha2.Tool{ + { + McpServer: &v1alpha2.McpServerTool{ + ToolNames: []string{"get-pods", "describe-pod"}, + }, + }, + { + McpServer: &v1alpha2.McpServerTool{ + ToolNames: []string{"helm-install"}, + }, + }, + }, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{"ghcr.io/org/skill-k8s:v1", "ghcr.io/org/skill-helm"}, + GitRefs: []v1alpha2.GitRepo{ + {URL: "https://github.com/org/my-skills.git", Name: "custom-skills"}, + {URL: "https://github.com/org/other-repo.git"}, + }, + }, + }, + }, + wantCtx: PromptTemplateContext{ + AgentName: "my-agent", + AgentNamespace: "production", + Description: "A helpful agent", + ToolNames: []string{"get-pods", "describe-pod", "helm-install"}, + SkillNames: []string{"skill-k8s", "skill-helm", "custom-skills", "other-repo"}, + }, + }, + { + name: "agent with no tools or skills", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple-agent", + Namespace: "default", + }, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "Simple", + Declarative: &v1alpha2.DeclarativeAgentSpec{}, + }, + }, + wantCtx: PromptTemplateContext{ + AgentName: "simple-agent", + AgentNamespace: "default", + Description: "Simple", + ToolNames: nil, + SkillNames: nil, + }, + }, + { + name: "agent with agent-type tools (not MCP)", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "orchestrator", + Namespace: "default", + }, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Tools: []*v1alpha2.Tool{ + { + Agent: &v1alpha2.TypedLocalReference{Name: "sub-agent"}, + }, + }, + }, + }, + }, + wantCtx: PromptTemplateContext{ + AgentName: "orchestrator", + AgentNamespace: "default", + ToolNames: nil, + SkillNames: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildTemplateContext(tt.agent) + assert.Equal(t, tt.wantCtx, got) + }) + } +} diff --git a/go/internal/utils/config_map.go b/go/internal/utils/config_map.go index 9fc6a77cb..a7811ead7 100644 --- a/go/internal/utils/config_map.go +++ b/go/internal/utils/config_map.go @@ -8,6 +8,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// GetConfigMapData fetches all data from a ConfigMap. +func GetConfigMapData(ctx context.Context, c client.Client, ref client.ObjectKey) (map[string]string, error) { + configMap := &corev1.ConfigMap{} + if err := c.Get(ctx, ref, configMap); err != nil { + return nil, fmt.Errorf("failed to find ConfigMap %s: %v", ref.String(), err) + } + return configMap.Data, nil +} + // GetConfigMapValue fetches a value from a ConfigMap func GetConfigMapValue(ctx context.Context, c client.Client, ref client.ObjectKey, key string) (string, error) { configMap := &corev1.ConfigMap{} diff --git a/go/internal/utils/secret.go b/go/internal/utils/secret.go index 0fec44d73..782013e91 100644 --- a/go/internal/utils/secret.go +++ b/go/internal/utils/secret.go @@ -8,6 +8,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// GetSecretData fetches all data from a Secret, converting byte values to strings. +func GetSecretData(ctx context.Context, c client.Client, ref client.ObjectKey) (map[string]string, error) { + secret := &corev1.Secret{} + if err := c.Get(ctx, ref, secret); err != nil { + return nil, fmt.Errorf("failed to find Secret %s: %v", ref.String(), err) + } + data := make(map[string]string, len(secret.Data)) + for k, v := range secret.Data { + data[k] = string(v) + } + return data, nil +} + // GetSecretValue fetches a value from a Secret func GetSecretValue(ctx context.Context, c client.Client, ref client.ObjectKey, key string) (string, error) { secret := &corev1.Secret{} diff --git a/helm/kagent/templates/builtin-prompts-configmap.yaml b/helm/kagent/templates/builtin-prompts-configmap.yaml new file mode 100644 index 000000000..bde21068e --- /dev/null +++ b/helm/kagent/templates/builtin-prompts-configmap.yaml @@ -0,0 +1,85 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kagent-builtin-prompts + namespace: {{ .Release.Namespace }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +data: + skills-usage: | + ## Skills + + You have access to skills — pre-built capabilities loaded from external sources and + available as files in your skills directory (/skills). Each skill contains instructions, + prompts, or data that extend your abilities beyond your base training. + + When a user asks you to perform a task, check if any of the available skills match + the request. If a skill is relevant, read and follow its instructions carefully. + Skills may contain domain-specific knowledge, step-by-step procedures, or templates + that you should use to produce higher-quality responses. + + Always prefer using a skill over generating a response from scratch when a relevant + skill is available. + + tool-usage-best-practices: | + ## Tool Usage Best Practices + + When using tools, follow these guidelines: + + 1. **Understand before acting**: Read tool descriptions carefully before calling them. + Ensure you understand what each tool does and what parameters it expects. + 2. **Use the right tool**: Select the most specific tool for the task. Avoid using + general-purpose tools when a specialized one is available. + 3. **Handle errors gracefully**: If a tool call fails, analyze the error message, + adjust your parameters, and retry. Do not repeat the exact same failing call. + 4. **Minimize calls**: Plan your approach to minimize the number of tool calls needed. + Batch operations when possible and avoid redundant calls. + 5. **Validate results**: After receiving tool output, verify that the results make sense + before presenting them to the user or using them in subsequent operations. + + safety-guardrails: | + ## Safety Guardrails + + You must follow these safety guidelines in all interactions: + + 1. **Do not execute destructive operations** without explicit user confirmation. + This includes deleting resources, modifying production systems, or any action + that cannot be easily reversed. + 2. **Protect sensitive information**: Never expose secrets, credentials, API keys, + or other sensitive data in your responses. If you encounter such information, + redact it before presenting it to the user. + 3. **Stay within scope**: Only perform actions that are within your defined capabilities + and the user's stated intent. Do not take autonomous actions beyond what was requested. + 4. **Report uncertainties**: If you are unsure about an action or its consequences, + communicate your uncertainty to the user and ask for guidance before proceeding. + + kubernetes-context: | + ## Kubernetes Context + + You are operating within a Kubernetes cluster environment. Keep these principles in mind: + + 1. **Namespace awareness**: Always be aware of which namespace you are operating in. + Resources are namespace-scoped unless they are cluster-level resources. + 2. **Resource management**: When creating or modifying Kubernetes resources, follow + best practices for labels, annotations, and resource limits. + 3. **Least privilege**: Only request the minimum permissions needed to accomplish the task. + Do not escalate privileges or access resources outside your authorized scope. + 4. **Cluster safety**: Be cautious with operations that affect cluster-wide resources + or could impact other workloads. Always consider the blast radius of your actions. + + a2a-communication: | + ## Agent-to-Agent Communication + + You can communicate with other agents to accomplish complex tasks that span multiple + domains of expertise. When working with other agents: + + 1. **Clear delegation**: When delegating a task to another agent, provide clear and + complete context. Include all relevant information the other agent needs to + accomplish the task. + 2. **Result validation**: When receiving results from another agent, validate them + before incorporating them into your response or using them for further actions. + 3. **Error handling**: If an agent you are communicating with fails or returns an + unexpected result, handle the error gracefully. Consider retrying, using an + alternative approach, or informing the user. + 4. **Avoid circular delegation**: Do not create circular delegation chains where + agents repeatedly delegate tasks back to each other without making progress. From 5d15cf9aeb87b95eac961085a43a3bd63dca7757 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 13:22:17 +0000 Subject: [PATCH 02/14] refactor: rename promptSources to promptTemplate.dataSources Wrap the prompt sources list in a PromptTemplateSpec struct, changing the CRD field from `promptSources: []` to `promptTemplate.dataSources: []`. This groups template-related configuration under a single field and provides a natural extension point for future template settings. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- docs/architecture/prompt-templates.md | 69 ++++++++++--------- go/api/v1alpha2/agent_types.go | 23 ++++--- go/api/v1alpha2/zz_generated.deepcopy.go | 28 ++++++-- go/internal/controller/agent_controller.go | 12 ++-- .../translator/agent/adk_api_translator.go | 6 +- 5 files changed, 86 insertions(+), 52 deletions(-) diff --git a/docs/architecture/prompt-templates.md b/docs/architecture/prompt-templates.md index 20a187c19..de597f7cf 100644 --- a/docs/architecture/prompt-templates.md +++ b/docs/architecture/prompt-templates.md @@ -19,10 +19,11 @@ Templates are resolved at **reconciliation time** by the controller. The final, │ {{include "builtin/skills-usage"}} │ │ You are {{.AgentName}}. │ │ │ -│ promptSources: │ -│ - kind: ConfigMap │ -│ name: kagent-builtin-prompts │ -│ alias: builtin │ +│ promptTemplate: │ +│ dataSources: │ +│ - kind: ConfigMap │ +│ name: kagent-builtin-prompts │ +│ alias: builtin │ └──────────────────────┬───────────────────────────────────┘ │ reconciliation ▼ @@ -86,7 +87,7 @@ ConfigMaps are a well-understood Kubernetes primitive and sufficient for the ini ### Backwards compatibility -To avoid breaking existing agents that may have literal `{{` characters in their system messages, the `systemMessage` field is **only** treated as a template when `promptSources` is non-empty. If no prompt sources are configured, the system message is passed through as a plain string, preserving existing behavior. +To avoid breaking existing agents that may have literal `{{` characters in their system messages, the `systemMessage` field is **only** treated as a template when `promptTemplate` is set. If no prompt template is configured, the system message is passed through as a plain string, preserving existing behavior. ## Usage @@ -124,9 +125,10 @@ spec: Your purpose: {{.Description}} {{include "my-prompts/safety"}} - promptSources: - - kind: ConfigMap - name: my-prompts + promptTemplate: + dataSources: + - kind: ConfigMap + name: my-prompts tools: - type: McpServer mcpServer: @@ -154,13 +156,14 @@ Never expose secrets or credentials in your responses. Use the `alias` field to shorten include paths: ```yaml -promptSources: - - kind: ConfigMap - name: kagent-builtin-prompts - alias: builtin - - kind: ConfigMap - name: my-custom-prompts - alias: custom +promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin + - kind: ConfigMap + name: my-custom-prompts + alias: custom ``` Then in your system message: @@ -204,10 +207,11 @@ spec: {{include "builtin/safety-guardrails"}} {{include "builtin/kubernetes-context"}} - promptSources: - - kind: ConfigMap - name: kagent-builtin-prompts - alias: builtin + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin ``` ### Using multiple sources @@ -215,16 +219,17 @@ spec: You can reference multiple ConfigMaps and Secrets simultaneously: ```yaml -promptSources: - - kind: ConfigMap - name: kagent-builtin-prompts - alias: builtin - - kind: ConfigMap - name: team-prompts - alias: team - - kind: Secret - name: proprietary-prompts - alias: proprietary +promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin + - kind: ConfigMap + name: team-prompts + alias: team + - kind: Secret + name: proprietary-prompts + alias: proprietary ``` Then use any key from any source: @@ -238,7 +243,7 @@ systemMessage: | ### Available template variables -When `promptSources` is non-empty, the following variables are available in `systemMessage`: +When `promptTemplate` is set, the following variables are available in `systemMessage`: | Variable | Type | Source | |----------|------|--------| @@ -264,7 +269,7 @@ You have {{len .ToolNames}} tools configured. ## ConfigMap change detection -The agent controller watches ConfigMaps referenced by agents via `promptSources` or `systemMessageFrom`. When a ConfigMap's content changes, all agents referencing it are automatically re-reconciled, and their resolved system messages are updated. +The agent controller watches ConfigMaps referenced by agents via `promptTemplate.dataSources` or `systemMessageFrom`. When a ConfigMap's content changes, all agents referencing it are automatically re-reconciled, and their resolved system messages are updated. ## Error handling @@ -277,7 +282,7 @@ kubectl get agent my-agent -o jsonpath='{.status.conditions[?(@.type=="Accepted" ## Related Files -- [agent_types.go](../../go/api/v1alpha2/agent_types.go) — `PromptSource` type and `PromptSources` field +- [agent_types.go](../../go/api/v1alpha2/agent_types.go) — `PromptTemplateSpec`, `PromptSource` types - [template.go](../../go/internal/controller/translator/agent/template.go) — Template resolution logic - [template_test.go](../../go/internal/controller/translator/agent/template_test.go) — Unit tests - [adk_api_translator.go](../../go/internal/controller/translator/agent/adk_api_translator.go) — `resolveSystemMessage()` integration diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 209f58dc3..cf293ce09 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -119,22 +119,20 @@ type GitRepo struct { // +kubebuilder:validation:XValidation:rule="!has(self.systemMessage) || !has(self.systemMessageFrom)",message="systemMessage and systemMessageFrom are mutually exclusive" type DeclarativeAgentSpec struct { // SystemMessage is a string specifying the system message for the agent. - // When PromptSources is non-empty, this field is treated as a Go text/template + // When PromptTemplate is set, this field is treated as a Go text/template // with access to an {{include "source/key"}} function and agent context variables // such as {{.AgentName}}, {{.AgentNamespace}}, {{.Description}}, {{.ToolNames}}, and {{.SkillNames}}. // +optional SystemMessage string `json:"systemMessage,omitempty"` // SystemMessageFrom is a reference to a ConfigMap or Secret containing the system message. - // When PromptSources is non-empty, the resolved value is treated as a Go text/template. + // When PromptTemplate is set, the resolved value is treated as a Go text/template. // +optional SystemMessageFrom *ValueSource `json:"systemMessageFrom,omitempty"` - // PromptSources defines ConfigMaps or Secrets whose keys can be included in the systemMessage - // using Go template syntax: {{include "alias/key"}} or {{include "name/key"}}. - // When this field is non-empty, systemMessage is treated as a Go template with access to - // the include function and agent context variables. + // PromptTemplate enables Go text/template processing on the systemMessage field. + // When set, systemMessage is treated as a Go template with access to the include function + // and agent context variables. // +optional - // +kubebuilder:validation:MaxItems=20 - PromptSources []PromptSource `json:"promptSources,omitempty"` + PromptTemplate *PromptTemplateSpec `json:"promptTemplate,omitempty"` // The name of the model config to use. // If not specified, the default value is "default-model-config". // Must be in the same namespace as the Agent. @@ -223,6 +221,15 @@ type ContextSummarizerConfig struct { PromptTemplate *string `json:"promptTemplate,omitempty"` } +// PromptTemplateSpec configures prompt template processing for an agent's system message. +type PromptTemplateSpec struct { + // DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + // using Go template syntax: {{include "alias/key"}} or {{include "name/key"}}. + // +optional + // +kubebuilder:validation:MaxItems=20 + DataSources []PromptSource `json:"dataSources,omitempty"` +} + // PromptSource references a Kubernetes resource whose keys are available as prompt fragments. // Currently supports ConfigMap and Secret resources (core API group). // In systemMessage templates, use {{include "alias/key"}} (or {{include "name/key"}} if no alias is set) diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 6bc782ec9..1557ebb6d 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -449,10 +449,10 @@ func (in *DeclarativeAgentSpec) DeepCopyInto(out *DeclarativeAgentSpec) { *out = new(ValueSource) **out = **in } - if in.PromptSources != nil { - in, out := &in.PromptSources, &out.PromptSources - *out = make([]PromptSource, len(*in)) - copy(*out, *in) + if in.PromptTemplate != nil { + in, out := &in.PromptTemplate, &out.PromptTemplate + *out = new(PromptTemplateSpec) + (*in).DeepCopyInto(*out) } if in.Tools != nil { in, out := &in.Tools, &out.Tools @@ -951,6 +951,26 @@ func (in *PromptSource) DeepCopy() *PromptSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PromptTemplateSpec) DeepCopyInto(out *PromptTemplateSpec) { + *out = *in + if in.DataSources != nil { + in, out := &in.DataSources, &out.DataSources + *out = make([]PromptSource, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromptTemplateSpec. +func (in *PromptTemplateSpec) DeepCopy() *PromptTemplateSpec { + if in == nil { + return nil + } + out := new(PromptTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteMCPServer) DeepCopyInto(out *RemoteMCPServer) { *out = *in diff --git a/go/internal/controller/agent_controller.go b/go/internal/controller/agent_controller.go index d943ca963..12cdd7869 100644 --- a/go/internal/controller/agent_controller.go +++ b/go/internal/controller/agent_controller.go @@ -357,11 +357,13 @@ func (r *AgentController) findAgentsReferencingConfigMap(ctx context.Context, cl } } - // Check if any promptSources reference this ConfigMap. - for _, ps := range agent.Spec.Declarative.PromptSources { - if ps.Kind == "ConfigMap" && ps.Name == obj.Name { - agents = append(agents, agent) - break + // Check if any promptTemplate dataSources reference this ConfigMap. + if pt := agent.Spec.Declarative.PromptTemplate; pt != nil { + for _, ds := range pt.DataSources { + if ds.Kind == "ConfigMap" && ds.Name == obj.Name { + agents = append(agents, agent) + break + } } } } diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index 13745f252..364c19687 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -721,12 +721,12 @@ func (a *adkApiTranslator) resolveSystemMessage(ctx context.Context, agent *v1al return "", fmt.Errorf("at least one system message source (SystemMessage or SystemMessageFrom) must be specified") } - // Only treat as a template when promptSources is configured (backwards compatible). - if len(agent.Spec.Declarative.PromptSources) == 0 { + // Only treat as a template when promptTemplate is configured (backwards compatible). + if agent.Spec.Declarative.PromptTemplate == nil || len(agent.Spec.Declarative.PromptTemplate.DataSources) == 0 { return rawMessage, nil } - lookup, err := resolvePromptSources(ctx, a.kube, agent.Namespace, agent.Spec.Declarative.PromptSources) + lookup, err := resolvePromptSources(ctx, a.kube, agent.Namespace, agent.Spec.Declarative.PromptTemplate.DataSources) if err != nil { return "", fmt.Errorf("failed to resolve prompt sources: %w", err) } From ac68a25320646327bda88afd35c513c02211f66e Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 13:28:12 +0000 Subject: [PATCH 03/14] test: add E2E test for prompt template feature Tests the full flow: creating a ConfigMap with prompt fragments, creating an Agent with promptTemplate.dataSources referencing it, verifying the agent reaches Ready state, and confirming that a ConfigMap update triggers re-reconciliation while the agent remains functional. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- go/test/e2e/invoke_api_test.go | 124 +++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 7 deletions(-) diff --git a/go/test/e2e/invoke_api_test.go b/go/test/e2e/invoke_api_test.go index 0bf844121..7495fe84a 100644 --- a/go/test/e2e/invoke_api_test.go +++ b/go/test/e2e/invoke_api_test.go @@ -134,13 +134,14 @@ func setupAgent(t *testing.T, cli client.Client, modelConfigName string, tools [ // AgentOptions provides optional configuration for agent setup type AgentOptions struct { - Name string - SystemMessage string - Stream bool - Env []corev1.EnvVar - Skills *v1alpha2.SkillForAgent - ExecuteCode *bool - Memory *v1alpha2.MemorySpec + Name string + SystemMessage string + Stream bool + Env []corev1.EnvVar + Skills *v1alpha2.SkillForAgent + ExecuteCode *bool + Memory *v1alpha2.MemorySpec + PromptTemplate *v1alpha2.PromptTemplateSpec } // setupAgentWithOptions creates and returns an agent resource with custom options @@ -419,6 +420,10 @@ func generateAgent(modelConfigName string, tools []*v1alpha2.Tool, opts AgentOpt agent.Spec.Declarative.Memory = opts.Memory } + if opts.PromptTemplate != nil { + agent.Spec.Declarative.PromptTemplate = opts.PromptTemplate + } + return agent } @@ -1020,6 +1025,111 @@ func TestE2EMemoryWithAgent(t *testing.T) { }) } +func TestE2EInvokeAgentWithPromptTemplate(t *testing.T) { + // Setup mock server — reuses the inline agent mock since the system prompt + // content doesn't affect mock matching; the test verifies that the controller + // correctly resolves promptTemplate and the agent reaches Ready state. + baseURL, stopServer := setupMockServer(t, "mocks/invoke_inline_agent.json") + defer stopServer() + + cli := setupK8sClient(t, false) + + // Create a ConfigMap with prompt fragments + promptConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-prompt-templates-", + Namespace: "kagent", + }, + Data: map[string]string{ + "preamble": "You are a helpful Kubernetes assistant. Always explain your reasoning.", + "safety": "Never delete resources without explicit user confirmation.", + }, + } + err := cli.Create(t.Context(), promptConfigMap) + require.NoError(t, err) + cleanup(t, cli, promptConfigMap) + + // Define tools + tools := []*v1alpha2.Tool{ + { + Type: v1alpha2.ToolProviderType_McpServer, + McpServer: &v1alpha2.McpServerTool{ + TypedLocalReference: v1alpha2.TypedLocalReference{ + ApiGroup: "kagent.dev", + Kind: "RemoteMCPServer", + Name: "kagent-tool-server", + }, + ToolNames: []string{"k8s_get_resources"}, + }, + }, + } + + modelCfg := setupModelConfig(t, cli, baseURL) + + // Create agent with promptTemplate using include directives and variable interpolation + agent := setupAgentWithOptions(t, cli, modelCfg.Name, tools, AgentOptions{ + Name: "prompt-tpl-test", + SystemMessage: `{{include "prompts/preamble"}} + +You are {{.AgentName}}, operating in {{.AgentNamespace}}. + +{{include "prompts/safety"}}`, + PromptTemplate: &v1alpha2.PromptTemplateSpec{ + DataSources: []v1alpha2.PromptSource{ + { + TypedLocalReference: v1alpha2.TypedLocalReference{ + Kind: "ConfigMap", + Name: promptConfigMap.Name, + }, + Alias: "prompts", + }, + }, + }, + }) + + a2aClient := setupA2AClient(t, agent) + + t.Run("sync_invocation", func(t *testing.T) { + runSyncTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane", nil) + }) + + t.Run("streaming_invocation", func(t *testing.T) { + runStreamingTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane") + }) + + // Test that updating the ConfigMap triggers re-reconciliation + t.Run("configmap_update_triggers_rereconcile", func(t *testing.T) { + // Update the ConfigMap + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + cm := &corev1.ConfigMap{} + if err := cli.Get(t.Context(), client.ObjectKeyFromObject(promptConfigMap), cm); err != nil { + return err + } + cm.Data["preamble"] = "You are an updated Kubernetes assistant." + return cli.Update(t.Context(), cm) + }) + require.NoError(t, err) + + // Wait for agent to re-reconcile by checking that it remains Ready + // (the controller watches ConfigMaps and re-reconciles referencing agents) + time.Sleep(5 * time.Second) + updatedAgent := &v1alpha2.Agent{} + err = cli.Get(t.Context(), client.ObjectKeyFromObject(agent), updatedAgent) + require.NoError(t, err) + + // Verify agent is still accepted and ready after ConfigMap update + for _, cond := range updatedAgent.Status.Conditions { + if cond.Type == v1alpha2.AgentConditionTypeAccepted { + require.Equal(t, string(metav1.ConditionTrue), string(cond.Status), + "Agent should remain Accepted after ConfigMap update, got: %s", cond.Message) + } + } + + // Verify the agent still responds correctly + runSyncTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane", nil) + }) +} + func TestE2EIAgentRunsCode(t *testing.T) { t.Skip("see issue.. TODO add issue here") // Setup mock server From 32cdbe6d3a4490a408459ab69314a49d197aa353 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 13:41:53 +0000 Subject: [PATCH 04/14] refactor: use built-in prompt templates in all agent helm charts Refactor all 10 agent helm charts to use the new promptTemplate feature, replacing repeated safety, operational, and tool-usage sections with includes from the kagent-builtin-prompts ConfigMap. Changes per agent: - k8s, helm: replaced Investigation Protocol, Safety Protocols, added tool-usage-best-practices - cilium-debug, cilium-manager: replaced Operational Protocol, Safety Guidelines - istio: replaced Operational Protocol, Safety Guidelines - kgateway: added safety-guardrails include - observability: replaced Operational Guidelines with kubernetes-context and tool-usage-best-practices - cilium-policy, argo-rollouts, promql: added minimal includes while preserving domain-specific reference material Also enhanced the built-in prompts ConfigMap with more substantive content based on patterns observed across all agents. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- .../agents/argo-rollouts/templates/agent.yaml | 9 +- helm/agents/cilium-debug/templates/agent.yaml | 61 ++--------- .../cilium-manager/templates/agent.yaml | 45 ++------ .../agents/cilium-policy/templates/agent.yaml | 8 +- helm/agents/helm/templates/agent.yaml | 64 ++--------- helm/agents/istio/templates/agent.yaml | 46 ++------ helm/agents/k8s/templates/agent.yaml | 53 ++------- helm/agents/kgateway/templates/agent.yaml | 8 +- .../agents/observability/templates/agent.yaml | 39 ++----- helm/agents/promql/templates/agent.yaml | 9 +- .../templates/builtin-prompts-configmap.yaml | 102 +++++++++++------- 11 files changed, 148 insertions(+), 296 deletions(-) diff --git a/helm/agents/argo-rollouts/templates/agent.yaml b/helm/agents/argo-rollouts/templates/agent.yaml index 6acb4aad5..cb8353172 100644 --- a/helm/agents/argo-rollouts/templates/agent.yaml +++ b/helm/agents/argo-rollouts/templates/agent.yaml @@ -9,10 +9,12 @@ spec: description: The Argo Rollouts Converter AI Agent specializes in converting Kubernetes Deployments to Argo Rollouts. type: Declarative declarative: - systemMessage: |- + systemMessage: | You are an Argo Rollouts specialist focused on progressive delivery and deployment automation. You are only responsible for defining the YAML for the Argo Rollout resource and simple kubectl argo rollouts commands. + {{ `{{include "builtin/safety-guardrails"}}` }} + Your key responsibility is assisting users with migrating their Kubernetes deployments to Argo Rollouts: - Convert Kubernetes deployments to Argo Rollout resources - Define the Argo Rollout resource YAML @@ -111,6 +113,11 @@ spec: should run next to the Deployment before deleting the Deployment or scaling down the Deployment. Not following this approach might result in downtime. It also allows the Rollout to be tested before deleting the original Deployment. Always follow this recommended approach unless the user specifies otherwise. + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/cilium-debug/templates/agent.yaml b/helm/agents/cilium-debug/templates/agent.yaml index 32f5e678e..a3a85760f 100644 --- a/helm/agents/cilium-debug/templates/agent.yaml +++ b/helm/agents/cilium-debug/templates/agent.yaml @@ -11,59 +11,16 @@ spec: declarative: modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} systemMessage: | - You are the Cilium Debug Agent, a specialized assistant for debugging, troubleshooting, - and advanced diagnostics of Cilium installations in Kubernetes clusters. + You are the Cilium Debug Agent, a specialized assistant for debugging, troubleshooting, + and advanced diagnostics of Cilium installations in Kubernetes clusters. Your primary responsibility is to help users diagnose and resolve issues with their Cilium deployments. ## Operational Protocol - When helping users troubleshoot Cilium issues, follow this structured approach: - - 1. **Initial Assessment**: - - Gather information about the Cilium version, deployment method, and cluster details - - Understand the specific symptoms or errors the user is experiencing - - Use diagnostic commands to examine the current state of Cilium components - - 2. **Systematic Diagnosis**: - - Use endpoint tools to inspect endpoint health, configuration, and logs - - Examine identity information to troubleshoot security and policy issues - - Analyze network connectivity using Envoy config, IP addresses, and IP cache - - Monitor BPF events and metrics for performance and behavioral anomalies - - Leverage PCAP recording for detailed traffic analysis when necessary - - 3. **Focused Remediation**: - - Request comprehensive debugging information for complex issues - - Identify root causes through methodical elimination - - Recommend targeted fixes based on diagnostic findings - - Guide users through repair procedures - - ## Safety Guidelines - When debugging Cilium, adhere to these safety principles: - - 1. **Minimize Disruption**: - - Prioritize read-only diagnostic tools before suggesting any modifications - - Warn about operations that could interrupt network connectivity (like encryption state changes) - - For critical actions, recommend testing in non-production environments first - - 2. **Data Protection**: - - Be cautious when suggesting commands that might expose sensitive data - - Advise users to redact output that might contain secrets or PII - - 3. **Progressive Approach**: - - Start with endpoint and identity inspection tools - - Progress to network and monitoring tools for deeper analysis - - Use PCAP recording only when necessary for detailed traffic inspection - - Consider encryption state tools only when security issues are suspected - - 4. **Tool Awareness**: - - You have access to a focused set of debugging tools covering: - - Endpoint management and health - - Identity information - - Network configuration and state - - Monitoring and metrics - - PCAP recording for traffic analysis - - Encryption state management - - For more advanced operations, guide users to the appropriate Cilium CLI commands - + + {{ `{{include "builtin/kubernetes-context"}}` }} + + {{ `{{include "builtin/safety-guardrails"}}` }} + ## Important Considerations - Always verify you're executing commands in the correct namespace (typically kube-system) - Cilium functionality relies heavily on kernel features; verify compatibility @@ -112,6 +69,10 @@ spec: - `cilium-dbg bpf recorder update ` - Update PCAP recorder - `cilium-dbg bpf recorder delete ` - Delete PCAP recorder + promptTemplate: + - configMapRef: + name: kagent-builtin-prompts + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/cilium-manager/templates/agent.yaml b/helm/agents/cilium-manager/templates/agent.yaml index b9ed34843..2a6ad65bc 100644 --- a/helm/agents/cilium-manager/templates/agent.yaml +++ b/helm/agents/cilium-manager/templates/agent.yaml @@ -26,44 +26,9 @@ spec: ## Operational Protocol - 1. **Initial Assessment** - - Gather information about the cluster and Cilium state - - Identify the scope and nature of the task or issue - - Determine required permissions and access levels - - Plan the approach with safety and minimal disruption - - 2. **Execution Strategy** - - Use read-only operations first for information gathering - - Validate planned changes before execution - - Implement changes incrementally when possible - - Verify results after each significant change - - Document all actions and outcomes - - 3. **Troubleshooting Methodology** - - Systematically narrow down problem sources - - Analyze logs, events, and metrics - - Check endpoint configurations and connectivity - - Verify BPF maps and policy enforcement - - Review recent changes and deployments - - Isolate service connectivity issues - - ## Safety Guidelines - - 1. **Cluster Operations** - - Prioritize non-disruptive operations - - Verify contexts before executing changes - - Understand blast radius of all operations - - Backup critical configurations before modifications - - Consider scaling implications of all changes - - Use canary deployments for Cilium upgrades - - 2. **Cilium Management** - - Test configuration changes in non-production environments first - - Verify connectivity before and after changes - - Gradually roll out major configuration changes - - Monitor for unexpected side effects after modifications - - Maintain fallback configurations for critical components - - Ensure proper resource allocation for Cilium components + {{ `{{include "builtin/kubernetes-context"}}` }} + + {{ `{{include "builtin/safety-guardrails"}}` }} ## Available Tools You have access to the following Cilium management tools: @@ -327,6 +292,10 @@ spec: - Use `cilium status --verbose` to get detailed agent status (or `cilium-dbg status --verbose`) - The `cilium monitor` tool is invaluable for real-time traffic analysis - For persistent issues, collect debug info with `cilium bugtool` + promptTemplate: + - configMapRef: + name: kagent-builtin-prompts + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/cilium-policy/templates/agent.yaml b/helm/agents/cilium-policy/templates/agent.yaml index 905c8f2c3..2097d7a22 100644 --- a/helm/agents/cilium-policy/templates/agent.yaml +++ b/helm/agents/cilium-policy/templates/agent.yaml @@ -13,6 +13,8 @@ spec: systemMessage: |- You are a CiliumNetworkPolicy and CiliumClusterwideNetworkPolicy agent that knows how to create valid YAML configurations based on user request. + {{ `{{include "builtin/safety-guardrails"}}` }} + ## Guidelines - Use "policy" for the resource name, if one is not provided. If a user provides a resource name, use that name. - You can only create CiliumNetworkPolicy and CiliumClusterwideNetworkPolicy resources. If you're unsure which resource needs creating, ask the user for clarification @@ -449,7 +451,11 @@ spec: 2. **ValidateCiliumNetworkPolicies** - Validates your network policies to ensure they are correctly formatted and will work as expected before applying them to your cluster These tools can be used to troubleshoot policy issues, verify policy behavior, and ensure your network policies are correctly configured before deployment. - + + promptTemplate: + - configMapRef: + name: kagent-builtin-prompts + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/helm/templates/agent.yaml b/helm/agents/helm/templates/agent.yaml index fc425f9d8..081b40648 100644 --- a/helm/agents/helm/templates/agent.yaml +++ b/helm/agents/helm/templates/agent.yaml @@ -24,43 +24,7 @@ spec: ## Operational Guidelines - ### Investigation Protocol - - 1. **Start With Information Gathering**: Begin with listing releases and checking statuses before suggesting modifications. - 2. **Progressive Approach**: Escalate to more complex operations only when necessary. - 3. **Document Everything**: Maintain a clear record of all recommended commands and actions. - 4. **Verify Before Acting**: Consider potential impacts before executing upgrades or changes. - 5. **Rollback Planning**: Always discuss rollback strategies for Helm operations. - - ### Problem-Solving Framework - - 1. **Initial Assessment** - - Check existing Helm releases in the cluster - - Verify Helm and chart versions - - Review release history and status - - Identify recent changes or upgrades - - 2. **Problem Classification** - - Chart configuration issues - - Release management problems - - Repository synchronization errors - - Upgrade/rollback failures - - Template rendering issues - - Resource conflicts - - 3. **Release Analysis** - - Manifest inspection - - Values configuration review - - Hooks examination - - Resource status verification - - Dependency validation - - 4. **Solution Implementation** - - Propose appropriate Helm operations - - Provide value overrides when needed - - Suggest chart modifications - - Present upgrade strategies - - Include rollback options + {{ `{{include "builtin/kubernetes-context"}}` }} ## Available Tools @@ -81,27 +45,13 @@ spec: ### Documentation Tools - `query_documentation`: Search documentation related to Helm, charts, and Kubernetes integration. - ## Safety Protocols - - 1. **Information First**: Always check the current state of releases before suggesting modifications. - 2. **Explain Operations**: Before recommending any Helm command, explain what it will do and potential impacts. - 3. **Dry-Run When Possible**: Suggest using `--dry-run` flags with upgrade operations. - 4. **Backup Values**: Recommend extracting current values with `GetRelease` before upgrades. - 5. **Release History Awareness**: Check release history before suggesting upgrades. - 6. **Namespace Scope**: Be explicit about namespaces in all operations. - 7. **Repository Validation**: Verify repositories are added and updated before operations. + ## Tool Usage Best Practices - ## Response Format + {{ `{{include "builtin/tool-usage-best-practices"}}` }} - When responding to user queries: + ## Safety Protocols - 1. **Initial Assessment**: Acknowledge the request and establish what you understand about the situation. - 2. **Information Gathering**: If needed, state what additional information you require about current releases. - 3. **Analysis**: Provide your analysis of the Helm release situation in clear, technical terms. - 4. **Recommendations**: Offer specific recommendations and the tools you'll use. - 5. **Action Plan**: Present a step-by-step plan for managing the Helm releases. - 6. **Verification**: Explain how to verify the release is working correctly after changes. - 7. **Knowledge Sharing**: Include brief explanations of relevant Helm concepts and best practices. + {{ `{{include "builtin/safety-guardrails"}}` }} ## Common Helm Operations @@ -145,6 +95,10 @@ spec: 4. You cannot access external systems outside the Kubernetes cluster unless through configured repositories. Always prioritize stability and correctness in Helm operations, and provide clear guidance on how to verify the success of operations. + promptTemplate: + - configMapRef: + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/istio/templates/agent.yaml b/helm/agents/istio/templates/agent.yaml index 0a0f0f877..c9ecf2218 100644 --- a/helm/agents/istio/templates/agent.yaml +++ b/helm/agents/istio/templates/agent.yaml @@ -9,7 +9,7 @@ spec: description: An Istio Expert AI Agent specializing in Istio operations, troubleshooting, and maintenance. type: Declarative declarative: - systemMessage: |- + systemMessage: | You are a Kubernetes and Istio Expert AI Agent with comprehensive knowledge of container orchestration, service mesh architecture, and cloud-native systems. You have access to a wide range of specialized tools that enable you to interact with Kubernetes clusters and Istio service mesh implementations to perform diagnostics, configuration, management, and troubleshooting. Core Expertise: @@ -70,44 +70,9 @@ spec: 4. Documentation and Information: - `query_documentation`: Query documentation and best practices - Operational Protocol: + {{ `{{include "builtin/kubernetes-context"}}` }} - 1. Initial Assessment - - Gather information about the cluster and relevant resources - - Identify the scope and nature of the task or issue - - Determine required permissions and access levels - - Plan the approach with safety and minimal disruption - - 2. Execution Strategy - - Use read-only operations first for information gathering - - Validate planned changes before execution - - Implement changes incrementally when possible - - Verify results after each significant change - - Document all actions and outcomes - - 3. Troubleshooting Methodology - - Systematically narrow down problem sources - - Analyze logs, events, and metrics - - Check resource configurations and relationships - - Verify network connectivity and policies - - Review recent changes and deployments - - Isolate service mesh configuration issues - - Safety Guidelines: - - 1. Cluster Operations - - Prioritize non-disruptive operations - - Verify contexts before executing changes - - Understand blast radius of all operations - - Backup critical configurations before modifications - - Consider scaling implications of all changes - - 2. Service Mesh Management - - Test Istio changes in isolated namespaces first - - Verify mTLS and security policies before implementation - - Gradually roll out traffic routing changes - - Monitor for unexpected side effects - - Maintain fallback configurations + {{ `{{include "builtin/safety-guardrails"}}` }} Best Practices: @@ -152,6 +117,11 @@ spec: - Multi-cluster connectivity issues Your primary goal is to provide expert assistance with Kubernetes and Istio environments by leveraging your specialized tools while following best practices for safety, reliability, and performance. Always aim to not just solve immediate issues but to improve the overall system architecture and operational practices. + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/k8s/templates/agent.yaml b/helm/agents/k8s/templates/agent.yaml index bc338dbba..5e60a8830 100644 --- a/helm/agents/k8s/templates/agent.yaml +++ b/helm/agents/k8s/templates/agent.yaml @@ -24,42 +24,7 @@ spec: ## Operational Guidelines - ### Investigation Protocol - - 1. **Start Non-Intrusively**: Begin with read-only operations (get, describe) before more invasive actions. - 2. **Progressive Escalation**: Escalate to more detailed investigation only when necessary. - 3. **Document Everything**: Maintain a clear record of all investigative steps and actions. - 4. **Verify Before Acting**: Consider potential impacts before executing any changes. - 5. **Rollback Planning**: Always have a plan to revert changes if needed. - - ### Problem-Solving Framework - - 1. **Initial Assessment** - - Gather basic cluster information - - Verify Kubernetes version and configuration - - Check node status and resource capacity - - Review recent changes or deployments - - 2. **Problem Classification** - - Application issues (crashes, scaling problems) - - Infrastructure problems (node failures, networking) - - Performance concerns (resource constraints, latency) - - Security incidents (policy violations, unauthorized access) - - Configuration errors (misconfigurations, invalid specs) - - 3. **Resource Analysis** - - Pod status and events - - Container logs - - Resource metrics - - Network connectivity - - Storage status - - 4. **Solution Implementation** - - Propose multiple solutions when appropriate - - Assess risks for each approach - - Present implementation plan - - Suggest testing strategies - - Include rollback procedures + {{ `{{include "builtin/kubernetes-context"}}` }} ## Available Tools @@ -88,15 +53,13 @@ spec: - `RemoveAnnotation`: Remove annotations from resources. - `GenerateResourceTool`: Generate YAML configurations for Istio, Gateway API, or Argo resources. + ## Tool Usage Best Practices + + {{ `{{include "builtin/tool-usage-best-practices"}}` }} + ## Safety Protocols - 1. **Read Before Write**: Always use informational tools first before modification tools. - 2. **Explain Actions**: Before using any modification tool, explain what you're doing and why. - 3. **Dry-Run When Possible**: Suggest using `--dry-run` flags when available. - 4. **Backup Current State**: Before modifications, suggest capturing the current state using `GetResourceYAML`. - 5. **Limited Scope**: Apply changes to the minimum scope necessary to fix the issue. - 6. **Verify Changes**: After any modification, verify the results with appropriate informational tools. - 7. **Avoid Dangerous Commands**: Do not execute potentially destructive commands without explicit confirmation. + {{ `{{include "builtin/safety-guardrails"}}` }} ## Response Format @@ -118,6 +81,10 @@ spec: 4. Remember that your suggestions impact production environments - prioritize safety and stability. Always start with the least intrusive approach, and escalate diagnostics only as needed. When in doubt, gather more information before recommending changes. + promptTemplate: + - configMapRef: + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/kgateway/templates/agent.yaml b/helm/agents/kgateway/templates/agent.yaml index 83ac07296..f833a1b38 100644 --- a/helm/agents/kgateway/templates/agent.yaml +++ b/helm/agents/kgateway/templates/agent.yaml @@ -12,6 +12,8 @@ spec: systemMessage: | You are kgateway Expert, a specialized AI assistant with deep knowledge of kgateway, the cloud-native API gateway built on top of Envoy proxy and the Kubernetes Gateway API. Your purpose is to help users with installing, configuring, and troubleshooting kgateway in their Kubernetes environments. + {{ `{{include "builtin/safety-guardrails"}}` }} + ## Your Expertise You are an expert in: @@ -108,7 +110,6 @@ spec: Always provide complete, precise YAML examples with accurate syntax. First gather contextual info: user's Kubernetes version, kgateway version, existing install state. Offer alternatives when applicable; explain pros and cons. - Recommend backups before modifying production environments. Educate users with explanations behind recommendations. Verify feature support against versions. Start with simple solutions before escalating complexity. @@ -202,6 +203,11 @@ spec: Always make sure to consult the official kgateway documentation using your query_documentation Tool for the most up-to-date information and best practices, even when the user does not ask for it. + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/observability/templates/agent.yaml b/helm/agents/observability/templates/agent.yaml index cb038e856..f652bb37b 100644 --- a/helm/agents/observability/templates/agent.yaml +++ b/helm/agents/observability/templates/agent.yaml @@ -9,7 +9,7 @@ spec: description: An Observability-oriented Agent specialized in using Prometheus, Grafana, and Kubernetes for monitoring and observability. This agent is equipped with a range of tools to query Prometheus for metrics, create Grafana dashboards, and verify Kubernetes resources. type: Declarative declarative: - systemMessage: |- + systemMessage: | # Observability AI Agent System Prompt You are an advanced AI agent specialized in Kubernetes observability with expertise in Prometheus monitoring and Grafana visualization. You excel at helping users design, implement, and troubleshoot monitoring solutions for Kubernetes environments. Your purpose is to assist users in gaining actionable insights from their infrastructure and application metrics through effective monitoring, querying, and visualization. @@ -22,37 +22,9 @@ spec: - **Metrics Interpretation**: You can analyze trends, anomalies, and correlations in observability data. - **Alerting Design**: You can recommend effective alerting strategies based on metrics and thresholds. - ## Operational Guidelines + {{ `{{include "builtin/kubernetes-context"}}` }} - ### Investigation Protocol - - 1. **Understand the Monitoring Objective**: Begin by clarifying what users want to observe or monitor. - 2. **Assess Current State**: Determine what monitoring infrastructure is already in place. - 3. **Progressive Approach**: Start with simple metrics and queries before moving to complex correlations. - 4. **Data-Driven Insights**: Base recommendations on actual metric data when available. - 5. **Visualization Best Practices**: Follow dashboard design principles for clarity and usefulness. - - ### Problem-Solving Framework - - 1. **Initial Assessment** - - Identify the observability goal (performance, availability, resource usage, etc.) - - Determine relevant components to monitor - - Assess existing monitoring configuration - - Understand the user's experience level with Prometheus and Grafana - - 2. **Problem Classification** - - Metric collection issues - - Query formulation challenges - - Dashboard design needs - - Alert configuration requirements - - Performance optimization concerns - - 3. **Solution Development** - - Generate appropriate PromQL queries - - Design effective visualizations - - Recommend dashboard structures - - Suggest alerting strategies - - Provide optimization guidance + {{ `{{include "builtin/tool-usage-best-practices"}}` }} ## Available Tools @@ -76,6 +48,11 @@ spec: - ALWAYS format your response as Markdown - Your response will include a summary of actions you took and an explanation of the result - If you created any artifacts such as files or resources, you will include those in your response as well + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/promql/templates/agent.yaml b/helm/agents/promql/templates/agent.yaml index 3de75eed0..6367875a0 100644 --- a/helm/agents/promql/templates/agent.yaml +++ b/helm/agents/promql/templates/agent.yaml @@ -10,11 +10,13 @@ spec: type: Declarative declarative: modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} - systemMessage: |- + systemMessage: | # PromQL Query Generator You are a specialized assistant that generates Prometheus Query Language (PromQL) queries based on natural language descriptions. Your primary function is to translate user intentions into precise, performant, and appropriate PromQL syntax. + {{ `{{include "builtin/tool-usage-best-practices"}}` }} + ## Your Capabilities 1. Generate syntactically correct PromQL queries from natural language descriptions @@ -153,6 +155,11 @@ spec: - Cross-environment comparison Remember that PromQL is designed for time series data and operates on a pull-based model with periodic scraping. Account for these characteristics when designing queries. + promptTemplate: + dataSources: + - kind: ConfigMap + name: kagent-builtin-prompts + alias: builtin a2aConfig: skills: - id: generate-promql-query diff --git a/helm/kagent/templates/builtin-prompts-configmap.yaml b/helm/kagent/templates/builtin-prompts-configmap.yaml index bde21068e..0b13741e3 100644 --- a/helm/kagent/templates/builtin-prompts-configmap.yaml +++ b/helm/kagent/templates/builtin-prompts-configmap.yaml @@ -24,48 +24,76 @@ data: tool-usage-best-practices: | ## Tool Usage Best Practices - When using tools, follow these guidelines: - - 1. **Understand before acting**: Read tool descriptions carefully before calling them. - Ensure you understand what each tool does and what parameters it expects. - 2. **Use the right tool**: Select the most specific tool for the task. Avoid using - general-purpose tools when a specialized one is available. - 3. **Handle errors gracefully**: If a tool call fails, analyze the error message, - adjust your parameters, and retry. Do not repeat the exact same failing call. - 4. **Minimize calls**: Plan your approach to minimize the number of tool calls needed. - Batch operations when possible and avoid redundant calls. - 5. **Validate results**: After receiving tool output, verify that the results make sense - before presenting them to the user or using them in subsequent operations. + Follow these principles when using tools: + + 1. **Read before write**: Always use informational tools (get, describe, list, status) + before modification tools. Understand the current state before making changes. + 2. **Explain before acting**: Before calling any modification tool, explain to the user + what you intend to do and why. Wait for confirmation on destructive operations. + 3. **Dry-run when possible**: Use dry-run flags or preview modes when available to + validate changes before applying them. + 4. **Use the right tool**: Select the most specific tool for the task. Check tool + descriptions carefully and ensure you understand the expected parameters. + 5. **Backup current state**: Before modifications, capture the current state (e.g., + export YAML, save configuration) so changes can be reverted if needed. + 6. **Verify after changes**: After any modification, use informational tools to confirm + the change took effect and didn't cause unintended side effects. + 7. **Handle errors gracefully**: If a tool call fails, analyze the error, adjust your + approach, and retry. Do not repeat the exact same failing call. + 8. **Minimize calls**: Plan your approach to minimize tool calls. Batch operations when + possible and avoid redundant calls. safety-guardrails: | - ## Safety Guardrails - - You must follow these safety guidelines in all interactions: - - 1. **Do not execute destructive operations** without explicit user confirmation. - This includes deleting resources, modifying production systems, or any action - that cannot be easily reversed. - 2. **Protect sensitive information**: Never expose secrets, credentials, API keys, - or other sensitive data in your responses. If you encounter such information, - redact it before presenting it to the user. - 3. **Stay within scope**: Only perform actions that are within your defined capabilities - and the user's stated intent. Do not take autonomous actions beyond what was requested. - 4. **Report uncertainties**: If you are unsure about an action or its consequences, + ## Safety Guidelines + + You must follow these safety principles in all interactions: + + 1. **No destructive operations without confirmation**: Never delete resources, modify + production systems, or take irreversible actions without explicit user confirmation. + Always explain what will happen and ask for approval first. + 2. **Least privilege**: Apply changes to the minimum scope necessary. Prefer targeted + operations over broad ones. Avoid cluster-wide changes when namespace-scoped changes + will suffice. + 3. **Rollback planning**: Before making changes, ensure you have a plan to revert them. + Capture the current state, explain the rollback procedure, and verify the rollback + path exists before proceeding. + 4. **Protect sensitive data**: Never expose secrets, credentials, API keys, tokens, or + certificates in your responses. Redact sensitive information before presenting output + to the user. + 5. **Stay within scope**: Only perform actions within your defined capabilities and the + user's stated intent. Do not take autonomous actions beyond what was requested. + 6. **Report uncertainties**: If you are unsure about an action or its consequences, communicate your uncertainty to the user and ask for guidance before proceeding. kubernetes-context: | - ## Kubernetes Context - - You are operating within a Kubernetes cluster environment. Keep these principles in mind: - - 1. **Namespace awareness**: Always be aware of which namespace you are operating in. - Resources are namespace-scoped unless they are cluster-level resources. - 2. **Resource management**: When creating or modifying Kubernetes resources, follow - best practices for labels, annotations, and resource limits. - 3. **Least privilege**: Only request the minimum permissions needed to accomplish the task. - Do not escalate privileges or access resources outside your authorized scope. - 4. **Cluster safety**: Be cautious with operations that affect cluster-wide resources - or could impact other workloads. Always consider the blast radius of your actions. + ## Kubernetes Operational Context + + You are operating within a Kubernetes cluster. Follow this methodology: + + ### Investigation Protocol + 1. **Start non-intrusively**: Begin with read-only operations (get, describe, logs, + events) before any modifications. Gather information before acting. + 2. **Progressive escalation**: Start with broad resource checks, then narrow down to + specific resources, pods, containers, or logs as you identify the problem area. + 3. **Verify before acting**: Consider the potential impact of any change. Check + dependencies, related resources, and downstream effects before modifying anything. + + ### Problem-Solving Framework + 1. **Initial assessment**: Check cluster health, node status, and recent events. Identify + the scope of the issue (single pod, deployment, namespace, or cluster-wide). + 2. **Classify the problem**: Determine if it's an application issue (crashes, errors), + infrastructure problem (node failures, resource exhaustion), networking issue + (connectivity, DNS, policies), or configuration error (invalid specs, missing secrets). + 3. **Analyze resources**: Examine pod status, container logs, resource metrics, events, + and network connectivity relevant to the issue. + 4. **Implement solutions**: Propose targeted fixes. Present multiple options when + appropriate, explain trade-offs, and include verification steps. + + ### Key Principles + - Always be explicit about which namespace you are operating in. + - Remember that your actions impact real workloads — prioritize stability. + - Use labels and selectors to target resources precisely. + - Check resource quotas and limits before creating or scaling resources. a2a-communication: | ## Agent-to-Agent Communication From 090ce6e817dd43a8aa006bf01fa27145540632d5 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 13:47:39 +0000 Subject: [PATCH 05/14] fix: resolve CI lint and CRD manifest issues - Replace sort.Strings with slices.Sort (sort package forbidden by depguard) - Remove Go template syntax ({{ }}) from CRD doc comments to prevent Helm from interpreting them as template directives during CRD installation - Regenerate CRD manifests and copy to helm chart Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- go/api/v1alpha2/agent_types.go | 10 ++-- go/config/crd/bases/kagent.dev_agents.yaml | 48 +++++++++++++++++-- .../controller/translator/agent/template.go | 4 +- .../templates/kagent.dev_agents.yaml | 48 +++++++++++++++++-- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index cf293ce09..bfe6408a0 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -120,8 +120,8 @@ type GitRepo struct { type DeclarativeAgentSpec struct { // SystemMessage is a string specifying the system message for the agent. // When PromptTemplate is set, this field is treated as a Go text/template - // with access to an {{include "source/key"}} function and agent context variables - // such as {{.AgentName}}, {{.AgentNamespace}}, {{.Description}}, {{.ToolNames}}, and {{.SkillNames}}. + // with access to an include("source/key") function and agent context variables + // such as .AgentName, .AgentNamespace, .Description, .ToolNames, and .SkillNames. // +optional SystemMessage string `json:"systemMessage,omitempty"` // SystemMessageFrom is a reference to a ConfigMap or Secret containing the system message. @@ -224,7 +224,7 @@ type ContextSummarizerConfig struct { // PromptTemplateSpec configures prompt template processing for an agent's system message. type PromptTemplateSpec struct { // DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage - // using Go template syntax: {{include "alias/key"}} or {{include "name/key"}}. + // using Go template syntax, e.g. include("alias/key") or include("name/key"). // +optional // +kubebuilder:validation:MaxItems=20 DataSources []PromptSource `json:"dataSources,omitempty"` @@ -232,7 +232,7 @@ type PromptTemplateSpec struct { // PromptSource references a Kubernetes resource whose keys are available as prompt fragments. // Currently supports ConfigMap and Secret resources (core API group). -// In systemMessage templates, use {{include "alias/key"}} (or {{include "name/key"}} if no alias is set) +// In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) // to insert the value of a specific key from this source. type PromptSource struct { // Inline reference to the Kubernetes resource. @@ -241,7 +241,7 @@ type PromptSource struct { TypedLocalReference `json:",inline"` // Alias is an optional short identifier for use in include directives. - // If set, use {{include "alias/key"}} instead of {{include "name/key"}}. + // If set, use include("alias/key") instead of include("name/key"). // +optional Alias string `json:"alias,omitempty"` } diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index 3c6dcbbb2..c2beb13d7 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -9941,18 +9941,58 @@ spec: If not specified, the default value is "default-model-config". Must be in the same namespace as the Agent. type: string + promptTemplate: + description: |- + PromptTemplate enables Go text/template processing on the systemMessage field. + When set, systemMessage is treated as a Go template with access to the include function + and agent context variables. + properties: + dataSources: + description: |- + DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + using Go template syntax, e.g. include("alias/key") or include("name/key"). + items: + description: |- + PromptSource references a Kubernetes resource whose keys are available as prompt fragments. + Currently supports ConfigMap and Secret resources (core API group). + In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) + to insert the value of a specific key from this source. + properties: + alias: + description: |- + Alias is an optional short identifier for use in include directives. + If set, use include("alias/key") instead of include("name/key"). + type: string + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + maxItems: 20 + type: array + type: object stream: description: |- Whether to stream the response from the model. If not specified, the default value is false. type: boolean systemMessage: - description: SystemMessage is a string specifying the system message - for the agent + description: |- + SystemMessage is a string specifying the system message for the agent. + When PromptTemplate is set, this field is treated as a Go text/template + with access to an include("source/key") function and agent context variables + such as .AgentName, .AgentNamespace, .Description, .ToolNames, and .SkillNames. type: string systemMessageFrom: - description: SystemMessageFrom is a reference to a ConfigMap or - Secret containing the system message. + description: |- + SystemMessageFrom is a reference to a ConfigMap or Secret containing the system message. + When PromptTemplate is set, the resolved value is treated as a Go text/template. properties: key: description: The key of the ConfigMap or Secret. diff --git a/go/internal/controller/translator/agent/template.go b/go/internal/controller/translator/agent/template.go index 4aed218bb..c2ca89497 100644 --- a/go/internal/controller/translator/agent/template.go +++ b/go/internal/controller/translator/agent/template.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "fmt" - "sort" + "slices" "strings" "text/template" @@ -119,7 +119,7 @@ func executeSystemMessageTemplate(rawMessage string, lookup map[string]string, t for k := range lookup { available = append(available, k) } - sort.Strings(available) + slices.Sort(available) return "", fmt.Errorf("prompt template %q not found in promptSources, available: %v", path, available) } return content, nil diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 3c6dcbbb2..c2beb13d7 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -9941,18 +9941,58 @@ spec: If not specified, the default value is "default-model-config". Must be in the same namespace as the Agent. type: string + promptTemplate: + description: |- + PromptTemplate enables Go text/template processing on the systemMessage field. + When set, systemMessage is treated as a Go template with access to the include function + and agent context variables. + properties: + dataSources: + description: |- + DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + using Go template syntax, e.g. include("alias/key") or include("name/key"). + items: + description: |- + PromptSource references a Kubernetes resource whose keys are available as prompt fragments. + Currently supports ConfigMap and Secret resources (core API group). + In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) + to insert the value of a specific key from this source. + properties: + alias: + description: |- + Alias is an optional short identifier for use in include directives. + If set, use include("alias/key") instead of include("name/key"). + type: string + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + maxItems: 20 + type: array + type: object stream: description: |- Whether to stream the response from the model. If not specified, the default value is false. type: boolean systemMessage: - description: SystemMessage is a string specifying the system message - for the agent + description: |- + SystemMessage is a string specifying the system message for the agent. + When PromptTemplate is set, this field is treated as a Go text/template + with access to an include("source/key") function and agent context variables + such as .AgentName, .AgentNamespace, .Description, .ToolNames, and .SkillNames. type: string systemMessageFrom: - description: SystemMessageFrom is a reference to a ConfigMap or - Secret containing the system message. + description: |- + SystemMessageFrom is a reference to a ConfigMap or Secret containing the system message. + When PromptTemplate is set, the resolved value is treated as a Go text/template. properties: key: description: The key of the ConfigMap or Secret. From eaecaeb6c83e8381abb29b1accae9d3eced46e9e Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 08:51:15 -0500 Subject: [PATCH 06/14] Update go/internal/controller/translator/agent/template_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eitan Yarmush --- .../translator/agent/template_test.go | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/go/internal/controller/translator/agent/template_test.go b/go/internal/controller/translator/agent/template_test.go index ef629fc97..377e01559 100644 --- a/go/internal/controller/translator/agent/template_test.go +++ b/go/internal/controller/translator/agent/template_test.go @@ -219,6 +219,55 @@ func TestBuildTemplateContext(t *testing.T) { SkillNames: []string{"skill-k8s", "skill-helm", "custom-skills", "other-repo"}, }, }, + { + name: "agent with skills using digests and git URLs with query/fragment", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-agent", + Namespace: "production", + }, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "A helpful agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Tools: []*v1alpha2.Tool{ + { + McpServer: &v1alpha2.McpServerTool{ + ToolNames: []string{"get-pods", "describe-pod"}, + }, + }, + { + McpServer: &v1alpha2.McpServerTool{ + ToolNames: []string{"helm-install"}, + }, + }, + }, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{ + "ghcr.io/org/skill-k8s@sha256:abcdef0123456789", + "ghcr.io/org/skill-helm:v1@sha256:0123456789abcdef", + }, + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills.git?ref=main#subdir", + Name: "custom-skills", + }, + { + URL: "https://github.com/org/other-repo.git?ref=dev#path/to/skills", + }, + }, + }, + }, + }, + wantCtx: PromptTemplateContext{ + AgentName: "my-agent", + AgentNamespace: "production", + Description: "A helpful agent", + ToolNames: []string{"get-pods", "describe-pod", "helm-install"}, + SkillNames: []string{"skill-k8s", "skill-helm", "custom-skills", "other-repo"}, + }, + }, { name: "agent with no tools or skills", agent: &v1alpha2.Agent{ From 88f03c51eab55f92b8188db251007399270f7242 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 14:05:19 +0000 Subject: [PATCH 07/14] refactor: address PR review feedback for prompt templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Drop Secret support from prompt sources — only ConfigMaps are supported to avoid security risks of referencing Secrets. 2. Adapt to TypedLocalReference (no namespace field) for PromptSource since prompt sources must be in the same namespace as the agent. 3. Move template resolution after the tool translation loop so tool names come from the already-built AgentConfig rather than being recomputed from the spec. 4. Use existing ociSkillName()/gitSkillName() helpers for skill name extraction instead of duplicating parsing logic. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- go/api/v1alpha2/agent_types.go | 22 +- go/api/v1alpha2/zz_generated.deepcopy.go | 19 +- .../mcp/frameworks/python/generator.go | 4 +- .../mcp/frameworks/typescript/generator.go | 4 +- .../internal/tui/dialogs/mcp_server_wizard.go | 2 +- go/cli/internal/tui/workspace.go | 4 +- go/config/crd/bases/kagent.dev_agents.yaml | 7 +- go/internal/controller/agent_controller.go | 2 +- .../controller/reconciler/file::memory: | Bin 0 -> 204800 bytes .../controller/reconciler/file::memory:-wal | 0 .../controller/reconciler/reconciler.go | 2 +- .../controller/reconciler/reconciler_test.go | 20 +- .../translator/agent/adk_api_translator.go | 54 ++-- .../agent/adk_api_translator_test.go | 26 +- .../translator/agent/mcp_validation_test.go | 12 +- .../controller/translator/agent/proxy_test.go | 10 +- .../controller/translator/agent/template.go | 61 ++--- .../translator/agent/template_test.go | 86 +++---- go/internal/database/service.go | 2 +- go/test/e2e/invoke_api_test.go | 10 +- .../templates/kagent.dev_agents.yaml | 7 +- metadata_plan.md | 235 +++++++++++++++++ plan.md | 238 ++++++++++++++++++ 23 files changed, 642 insertions(+), 185 deletions(-) create mode 100644 go/internal/controller/reconciler/file::memory: create mode 100644 go/internal/controller/reconciler/file::memory:-wal create mode 100644 metadata_plan.md create mode 100644 plan.md diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index bfe6408a0..ec347a101 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -223,21 +223,19 @@ type ContextSummarizerConfig struct { // PromptTemplateSpec configures prompt template processing for an agent's system message. type PromptTemplateSpec struct { - // DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + // DataSources defines the ConfigMaps whose keys can be included in the systemMessage // using Go template syntax, e.g. include("alias/key") or include("name/key"). // +optional // +kubebuilder:validation:MaxItems=20 DataSources []PromptSource `json:"dataSources,omitempty"` } -// PromptSource references a Kubernetes resource whose keys are available as prompt fragments. -// Currently supports ConfigMap and Secret resources (core API group). +// PromptSource references a ConfigMap whose keys are available as prompt fragments. // In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) // to insert the value of a specific key from this source. type PromptSource struct { // Inline reference to the Kubernetes resource. // For ConfigMaps: kind=ConfigMap, apiGroup="" (empty for core API group). - // For Secrets: kind=Secret, apiGroup="" (empty for core API group). TypedLocalReference `json:",inline"` // Alias is an optional short identifier for use in include directives. @@ -353,7 +351,7 @@ type Tool struct { // +optional McpServer *McpServerTool `json:"mcpServer,omitempty"` // +optional - Agent *TypedLocalReference `json:"agent,omitempty"` + Agent *TypedReference `json:"agent,omitempty"` // HeadersFrom specifies a list of configuration values to be added as // headers to requests sent to the Tool from this agent. The value of @@ -382,7 +380,7 @@ func (s *Tool) ResolveHeaders(ctx context.Context, client client.Client, namespa type McpServerTool struct { // The reference to the ToolServer that provides the tool. // +optional - TypedLocalReference `json:",inline"` + TypedReference `json:",inline"` // The names of the tools to be provided by the ToolServer // For a list of all the tools provided by the server, @@ -410,18 +408,26 @@ type TypedLocalReference struct { // +optional ApiGroup string `json:"apiGroup"` Name string `json:"name"` +} + +type TypedReference struct { + // +optional + Kind string `json:"kind"` + // +optional + ApiGroup string `json:"apiGroup"` + Name string `json:"name"` // +optional Namespace string `json:"namespace,omitempty"` } -func (t *TypedLocalReference) GroupKind() schema.GroupKind { +func (t *TypedReference) GroupKind() schema.GroupKind { return schema.GroupKind{ Group: t.ApiGroup, Kind: t.Kind, } } -func (t *TypedLocalReference) NamespacedName(defaultNamespace string) types.NamespacedName { +func (t *TypedReference) NamespacedName(defaultNamespace string) types.NamespacedName { namespace := t.Namespace if namespace == "" { namespace = defaultNamespace diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 1557ebb6d..fd5a984f3 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -582,7 +582,7 @@ func (in *MCPTool) DeepCopy() *MCPTool { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *McpServerTool) DeepCopyInto(out *McpServerTool) { *out = *in - out.TypedLocalReference = in.TypedLocalReference + out.TypedReference = in.TypedReference if in.ToolNames != nil { in, out := &in.ToolNames, &out.ToolNames *out = make([]string, len(*in)) @@ -1308,7 +1308,7 @@ func (in *Tool) DeepCopyInto(out *Tool) { } if in.Agent != nil { in, out := &in.Agent, &out.Agent - *out = new(TypedLocalReference) + *out = new(TypedReference) **out = **in } if in.HeadersFrom != nil { @@ -1345,6 +1345,21 @@ func (in *TypedLocalReference) DeepCopy() *TypedLocalReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TypedReference) DeepCopyInto(out *TypedReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypedReference. +func (in *TypedReference) DeepCopy() *TypedReference { + if in == nil { + return nil + } + out := new(TypedReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValueRef) DeepCopyInto(out *ValueRef) { *out = *in diff --git a/go/cli/internal/mcp/frameworks/python/generator.go b/go/cli/internal/mcp/frameworks/python/generator.go index becc060f0..2472c8b87 100644 --- a/go/cli/internal/mcp/frameworks/python/generator.go +++ b/go/cli/internal/mcp/frameworks/python/generator.go @@ -125,7 +125,7 @@ Do not edit manually - it will be overwritten when tools are loaded. // Add import statements for _, tool := range tools { - content.WriteString(fmt.Sprintf("from .%s import %s\n", tool, tool)) + fmt.Fprintf(&content, "from .%s import %s\n", tool, tool) } // Add empty line @@ -137,7 +137,7 @@ Do not edit manually - it will be overwritten when tools are loaded. if i > 0 { content.WriteString(", ") } - content.WriteString(fmt.Sprintf(`"%s"`, tool)) + fmt.Fprintf(&content, `"%s"`, tool) } content.WriteString("]\n") diff --git a/go/cli/internal/mcp/frameworks/typescript/generator.go b/go/cli/internal/mcp/frameworks/typescript/generator.go index ffbec4316..8fd81c171 100644 --- a/go/cli/internal/mcp/frameworks/typescript/generator.go +++ b/go/cli/internal/mcp/frameworks/typescript/generator.go @@ -134,7 +134,7 @@ func (g *Generator) generateToolsContent(tools []string) string { // Add import statements (named exports) for _, tool := range tools { - content.WriteString(fmt.Sprintf("import { %s } from './tools/%s.js';\n", tool, tool)) + fmt.Fprintf(&content, "import { %s } from './tools/%s.js';\n", tool, tool) } // Add empty line @@ -146,7 +146,7 @@ func (g *Generator) generateToolsContent(tools []string) string { if i > 0 { content.WriteString(",\n") } - content.WriteString(fmt.Sprintf(" %s", tool)) + fmt.Fprintf(&content, " %s", tool) } content.WriteString("\n];\n\n") diff --git a/go/cli/internal/tui/dialogs/mcp_server_wizard.go b/go/cli/internal/tui/dialogs/mcp_server_wizard.go index 373e3071e..3f87ae49b 100644 --- a/go/cli/internal/tui/dialogs/mcp_server_wizard.go +++ b/go/cli/internal/tui/dialogs/mcp_server_wizard.go @@ -944,7 +944,7 @@ func (w *McpServerWizard) renderHeadersStep() string { displayValue = v[:7] + "***" } } - sb.WriteString(fmt.Sprintf(" • %s: %s\n", k, displayValue)) + fmt.Fprintf(&sb, " • %s: %s\n", k, displayValue) } sb.WriteString("\n") } diff --git a/go/cli/internal/tui/workspace.go b/go/cli/internal/tui/workspace.go index dc0c93cab..cc2bac2df 100644 --- a/go/cli/internal/tui/workspace.go +++ b/go/cli/internal/tui/workspace.go @@ -218,7 +218,7 @@ func (m *workspaceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.loadSessions() case wsAgentsLoadedMsg: if msg.err != nil { - m.details.WriteString(fmt.Sprintf("Error: failed to load agents: %v", msg.err)) + fmt.Fprintf(&m.details, "Error: failed to load agents: %v", msg.err) return m, nil } // Sort and store agents for later; do not auto-open chooser or auto-select. @@ -230,7 +230,7 @@ func (m *workspaceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case loadAgentMsg: if msg.err != nil { - m.details.WriteString(fmt.Sprintf("Error: %v", msg.err)) + fmt.Fprintf(&m.details, "Error: %v", msg.err) return m, nil } a := msg.agent diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index c2beb13d7..ccdad1a95 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -9949,12 +9949,11 @@ spec: properties: dataSources: description: |- - DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + DataSources defines the ConfigMaps whose keys can be included in the systemMessage using Go template syntax, e.g. include("alias/key") or include("name/key"). items: description: |- - PromptSource references a Kubernetes resource whose keys are available as prompt fragments. - Currently supports ConfigMap and Secret resources (core API group). + PromptSource references a ConfigMap whose keys are available as prompt fragments. In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) to insert the value of a specific key from this source. properties: @@ -9969,8 +9968,6 @@ spec: type: string name: type: string - namespace: - type: string required: - name type: object diff --git a/go/internal/controller/agent_controller.go b/go/internal/controller/agent_controller.go index 12cdd7869..6687c9853 100644 --- a/go/internal/controller/agent_controller.go +++ b/go/internal/controller/agent_controller.go @@ -360,7 +360,7 @@ func (r *AgentController) findAgentsReferencingConfigMap(ctx context.Context, cl // Check if any promptTemplate dataSources reference this ConfigMap. if pt := agent.Spec.Declarative.PromptTemplate; pt != nil { for _, ds := range pt.DataSources { - if ds.Kind == "ConfigMap" && ds.Name == obj.Name { + if ds.Name == obj.Name { agents = append(agents, agent) break } diff --git a/go/internal/controller/reconciler/file::memory: b/go/internal/controller/reconciler/file::memory: new file mode 100644 index 0000000000000000000000000000000000000000..84d137c05978f935ec000736a747e8b3f3db61ca GIT binary patch literal 204800 zcmeI*&2Jm$oxpL9UO0NOEXRs6i4`TIxT!RiWtoy4=LJE^#*9~uEW3^b*trZDawJbB zkqkMqox?6dDcD~3(mfa0!=Bnh4@I$uc7gs0Ez)g)_P)IrSfB;kXNEJJXE;MvG}P>p z`xPvae4c0Kd7jVjnTIptGxA5@*|Z%?->uXR3y!{?x{w+gN_|t;Q>m09zSqQe@O4={ z8Trd!{^y#X;O2M#y82HKQsR45lv1Yu6P zPT$UcYcr?k!;HMXlrPq-f@77;f|J)v;+bO~T37V^QPq7BOD;sCv#X9>DK|4k`GZ2;F^iRQ*(!=UAF?u)8+LsY27D?Abn zHSJQc^^NYr-3Nnm}*0E!UR=r->v!tqC>{3-&I$oQUXB;oRplb8;$`_Zt!4&i& zc+{eUDo73nmR)wNJ*%e6@yqUP-`&^}+ug}+ZHvI2$(RX%Vsn7h7u|1-sMD^S)k@uV z?8jo-C?6eKHM^+a%6*XC+1%DY$Zmd=6NQ4#nC`q1pF!+;{m3$%&#L~+W89lfYs<^Z zLl4TIKI_(}M^?FLwY_{H!q%3#5;(1lOy{a^pTf?e)RDWcaD~`Qu7Ypv} zWjclWz6g3OV>TZBSeans<`{@&yJ7TqpEnvaGpcq+H0EZP#)K1AY-i7(raj^IU792n zGO`!b+RkdHSuxfKGZRaiX&gMKYOAZtiRwizRv?yeF%rFeEI%0i?e9kcWo(e1$sr)# zFAK3zDj#^>TyGe~3u3C$guXwU=v#$V+v*9PS`*6sMCb z2}9>?cA--xVY#+SqzuD2pVr>hI>o-(+Kl%w-PoN{HBD1amc1BziQbR%(O7zEo#H2T z&x?xTxRuI~_8=dP= zL0BqUfs}46PINubaeI?->zHi3H}0M19E*YPo_UHi_j@85Q8%emK>vP0k`D%dtRx(Y z5605k%AE3~HK7I$?mK}0d^}DmmufotDQ-W#lQv^tRkgR~l*V*y*I_LEjmA1x5SEQq zBV`#@S{&@6vhd_5XrVYC5i^Tj@?Wc+s|rfSDr#jIE2C*GJJ;!O530=gPtuJCBdRtx z7fkDZvi~p|9VC8gr*RUV2usIWFa?~+AZqop(_oP*7E}0e`|R4>J`9TYdL+)>UaraQ zrM4gF#!$D}wYj5=_b}YJp?I_F@rHLKdWqhTp(wMLCL@bpoe5uf7A9Z+iA%X%doQnl zw_YhgD(USKmY**5I_I{1Q0*~0R#{jRDlV%V_g3aYIx}v z0tg_000IagfB*srAbV){|7(D^bi3A5I_I{1Q0*~ z0R#|000FN583PC)fB*srAbwd400IagfB*srAbT>lS#jOif)2q1s} z0tg_000IagfB*ul|7Q#!fB*srAbfB*srAbn3qQtLNnQft@N-dMS|wz76}du{#gwVQ8e z)~?>X_QuUOZ@!Va{&6e!&F#!}k@fc4^{Y3o-&k9J^V<5GA3IjvS$$kMuuCh|O3hh0 zELQKlfA1r!_SmXj-7oA}W#?+idc3uobwB@Crhkhg0M3`u|Hq zU#6!2VS4`jPp5u2^`oiV=YD(c^U0r0et+`%#4pGHF#hxLkH`KvmR5hEuB3mE-Wt6; z^5cD0fTy2?`)eDbQQy|c23{ex|MsN=R0nZ*Y9rW`Ea|uzLXF5mp9g5No%(kl$2L*{nG>6 zvCP7eQ}JHQjmU)ic=dR6Uey*Bl#?B&@QG+!tEu6$SN!&EXTPb`LM8A+C!E0-=84z0ZISWg{&>+ZI`wX?Y?_OVy1997MI(foK`$*LD? zcGa;fyXu() zGa)olf6-*2FE_G17?qb)?VWk0G1Cghu4R=zDHQi5>LBS-JSahyRJs|MAfr7vyDzHR z4N;*Qukc7T)U->%mM?Xw%`Y0SD#$P@TE~tZTJ^eE+$2@?VwbAI((&4)JmYxj1y!4$ zSH8IH4W^(M!J`%(R6%kuuqnO9d{*^m z9^>9@T3cRL9(qvz^jWt)J+jJ0tL^0r5w^C>CHJ_yBDORd6VI#K?Nz0*+!|KZqxvJW zTygAOyI62%FViX1_eIcS8ME=|$I1j7H^)FM+YO_?`@GSZnNhVnqA@qSG$x#%V>^5P zH0=qu@6sfxkdeKZ)^=7q&5E%`n3-78Oyl4=Ra;$EPE;>)u>!G#i;?K%WBI}8Z+|}u zC}V^4Ob!9@ep!f(Qu)C1=6b^@UJz52CQnt?W3eRCtc&@>9Z$Ac8!28t7Y|o^xn_vG zRJY6F4)R8EI=PZCblzqcI#m*uYpX=cFpTqQ?Om-??3=C4cn{N!-6>VmH05O3i?NsJ z{Wu?urI*$zeo_a$5Q;gXLZ)}F8n(A(%iy@^O z=44_8p^dxIxegVCrJ@x`>Bizj*Yg~=HyO8%$;Nx*-g(Zk82Ij)r$}?ZC!!H`lR5?T z?-wNbVDQIE!lC$JEUm4~DNkAxYT)3$1NhI!T6V6};T}|( z@t>p{4@OjNZZ4SC{bc`PG&)H9)K23hJQ0?TwO|T3lR?z#Wv9U+RV=3P;r7|JxqTQE z@AXKWyS-eK+e>Xf(2b#Pvukrl8Si1ZaYOND*W(TENc0lDA45@QFHJ@ky*d-V@GMNe z{u7sSyY^mQ|8Bifz7jbH_1_Bl|Nl<;D+!7rfB*srAbyDq|EsyZiug2GI#J6j%-bmQmU1K4VgBA2fW9}QO_8rlR_u5+_*D0+M zOW5a1#};+iN7vScOI7kl&PZzy*E=ojNob0hNl3hMd~r!EO;E)A6m|%O34h@c}12Hg$I&+&q;50s5FPmDc0x)%@<3f!Ul^MauW@MX{9-d%2SUU ze0hNW-<(z)e}aG3p(>7vU|N6n(dfVTJnL{2<8N-U{SD<{&n!N&iu+Y@znEh_skwKL zh2coZ{YHFRN+>1mqd74pl!-)qZ^5_Vu_5)zG*>g(Pc%J-T#1|$?oNLq?cQ3)lU zIr5=yx!yL1wQgQ{?KO8zWmqz4wrj>l+$NY0Zi$hnJMov3j}mUC^OB|8UF1!Qj|$?Z zxr4Sgxigqpe4RMqp2aARE~xG_Z%P(IEruf!LJl#m7rv%y4;Pij-PVaxG=kMa&AnMd z+N)iPlvw;*qdI912_xGbC5c51>$0jXiYAUrO)OTu6N*SnzMj+)zhH+7rLc&%nAcP- zC+Z2$TqC=e4$MgU>q*thVBw_Dp%qdA}Nc5 z4TC$S#q`zNd1&$}?*G#}rTP57hu>sS6afSfKmY**5I_I{1Q0*~fj$-B`oB*DN3#(? z009ILKmY**5I_I{1Q6&!fb0JruqcWE0tg_000IagfB*srAb>!h3h?*;`!sMg8vz6m zKmY**5I_I{1Q0*~fgS|-{J#e*iXwmj0tg_000IagfB*srAke1*T>tlJ;Al1i2q1s} z0tg_000IagfB*tL2(bRY2P}#rfB*srAb 0 { + lookup, err := resolvePromptSources(ctx, a.kube, agent.Namespace, agent.Spec.Declarative.PromptTemplate.DataSources) if err != nil { - return "", err + return nil, nil, nil, fmt.Errorf("failed to resolve prompt sources: %w", err) } - rawMessage = msg - } else if agent.Spec.Declarative.SystemMessage != "" { - rawMessage = agent.Spec.Declarative.SystemMessage - } else { - return "", fmt.Errorf("at least one system message source (SystemMessage or SystemMessageFrom) must be specified") - } - // Only treat as a template when promptTemplate is configured (backwards compatible). - if agent.Spec.Declarative.PromptTemplate == nil || len(agent.Spec.Declarative.PromptTemplate.DataSources) == 0 { - return rawMessage, nil - } + tplCtx := buildTemplateContext(agent, cfg) - lookup, err := resolvePromptSources(ctx, a.kube, agent.Namespace, agent.Spec.Declarative.PromptTemplate.DataSources) - if err != nil { - return "", fmt.Errorf("failed to resolve prompt sources: %w", err) + resolved, err := executeSystemMessageTemplate(cfg.Instruction, lookup, tplCtx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to execute system message template: %w", err) + } + cfg.Instruction = resolved } - tplCtx := buildTemplateContext(agent) + return cfg, mdd, secretHashBytes, nil +} - resolved, err := executeSystemMessageTemplate(rawMessage, lookup, tplCtx) - if err != nil { - return "", fmt.Errorf("failed to execute system message template: %w", err) +// resolveRawSystemMessage gets the raw system message string from the agent spec +// without applying any template processing. +func (a *adkApiTranslator) resolveRawSystemMessage(ctx context.Context, agent *v1alpha2.Agent) (string, error) { + if agent.Spec.Declarative.SystemMessageFrom != nil { + return agent.Spec.Declarative.SystemMessageFrom.Resolve(ctx, a.kube, agent.Namespace) } - - return resolved, nil + if agent.Spec.Declarative.SystemMessage != "" { + return agent.Spec.Declarative.SystemMessage, nil + } + return "", fmt.Errorf("at least one system message source (SystemMessage or SystemMessageFrom) must be specified") } const ( diff --git a/go/internal/controller/translator/agent/adk_api_translator_test.go b/go/internal/controller/translator/agent/adk_api_translator_test.go index 4071fcc4f..fa9e09c1f 100644 --- a/go/internal/controller/translator/agent/adk_api_translator_test.go +++ b/go/internal/controller/translator/agent/adk_api_translator_test.go @@ -93,7 +93,7 @@ func Test_AdkApiTranslator_CrossNamespaceAgentTool(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "tool-agent", Namespace: "source-ns", }, @@ -134,7 +134,7 @@ func Test_AdkApiTranslator_CrossNamespaceAgentTool(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "tool-agent", Namespace: "target-ns", }, @@ -263,7 +263,7 @@ func Test_AdkApiTranslator_CrossNamespaceRemoteMCPServer(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Kind: "RemoteMCPServer", ApiGroup: "kagent.dev", Name: "tools-server", @@ -305,7 +305,7 @@ func Test_AdkApiTranslator_CrossNamespaceRemoteMCPServer(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Kind: "RemoteMCPServer", ApiGroup: "kagent.dev", Name: "tools-server", @@ -653,7 +653,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { leafAgents = append(leafAgents, leafAgent(name)) tools = append(tools, &v1alpha2.Tool{ Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: name, }, }) @@ -711,7 +711,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { agents[i].Spec.Declarative.Tools = []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: fmt.Sprintf("chain-%d", i+1), }, }, @@ -754,7 +754,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { agents[i].Spec.Declarative.Tools = []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: fmt.Sprintf("deep-%d", i+1), }, }, @@ -789,7 +789,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "cycle-b", }, }, @@ -811,7 +811,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "cycle-a", }, }, @@ -847,7 +847,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "diamond-d", }, }, @@ -869,7 +869,7 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "diamond-d", }, }, @@ -891,13 +891,13 @@ func Test_AdkApiTranslator_RecursionDepthTracking(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "diamond-b", }, }, { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "diamond-c", }, }, diff --git a/go/internal/controller/translator/agent/mcp_validation_test.go b/go/internal/controller/translator/agent/mcp_validation_test.go index 18bda8afe..0673ba9f6 100644 --- a/go/internal/controller/translator/agent/mcp_validation_test.go +++ b/go/internal/controller/translator/agent/mcp_validation_test.go @@ -70,7 +70,7 @@ func TestMCPServerValidation_InvalidPort(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-mcp-server", Kind: "MCPServer", }, @@ -155,7 +155,7 @@ func TestMCPServerValidation_ValidPort(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-mcp-server", Kind: "MCPServer", }, @@ -225,7 +225,7 @@ func TestMCPServerValidation_NotFound(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "non-existent-mcp-server", Kind: "MCPServer", }, @@ -364,7 +364,7 @@ func TestMCPServerValidation_RemoteMCPServer(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-remote-mcp", Kind: "RemoteMCPServer", }, @@ -512,7 +512,7 @@ func TestMCPServerValidation_MultipleTools(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "valid-mcp-server", Kind: "MCPServer", }, @@ -522,7 +522,7 @@ func TestMCPServerValidation_MultipleTools(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "invalid-mcp-server", Kind: "MCPServer", }, diff --git a/go/internal/controller/translator/agent/proxy_test.go b/go/internal/controller/translator/agent/proxy_test.go index 10bca85f4..05099ab97 100644 --- a/go/internal/controller/translator/agent/proxy_test.go +++ b/go/internal/controller/translator/agent/proxy_test.go @@ -74,14 +74,14 @@ func TestProxyConfiguration_ThroughTranslateAgent(t *testing.T) { Tools: []*v1alpha2.Tool{ { Type: v1alpha2.ToolProviderType_Agent, - Agent: &v1alpha2.TypedLocalReference{ + Agent: &v1alpha2.TypedReference{ Name: "nested-agent", }, }, { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-mcp", Kind: "RemoteMCPServer", }, @@ -218,7 +218,7 @@ func TestProxyConfiguration_RemoteMCPServer_ExternalURL(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "external-mcp", Kind: "RemoteMCPServer", }, @@ -310,7 +310,7 @@ func TestProxyConfiguration_MCPServer(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-mcp-server", Kind: "MCPServer", }, @@ -407,7 +407,7 @@ func TestProxyConfiguration_Service(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ Name: "test-service", Kind: "Service", }, diff --git a/go/internal/controller/translator/agent/template.go b/go/internal/controller/translator/agent/template.go index c2ca89497..e3aafad3a 100644 --- a/go/internal/controller/translator/agent/template.go +++ b/go/internal/controller/translator/agent/template.go @@ -5,11 +5,12 @@ import ( "context" "fmt" "slices" - "strings" "text/template" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/utils" + "github.com/kagent-dev/kagent/go/pkg/adk" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -27,7 +28,7 @@ type PromptTemplateContext struct { SkillNames []string } -// resolvePromptSources fetches all data from the referenced ConfigMaps/Secrets and builds +// resolvePromptSources fetches all data from the referenced ConfigMaps and builds // a lookup map keyed by "identifier/key" where identifier is the alias (if set) or resource name. func resolvePromptSources(ctx context.Context, kube client.Client, namespace string, sources []v1alpha2.PromptSource) (map[string]string, error) { lookup := make(map[string]string) @@ -38,19 +39,9 @@ func resolvePromptSources(ctx context.Context, kube client.Client, namespace str identifier = src.Alias } - nn := src.NamespacedName(namespace) + nn := types.NamespacedName{Namespace: namespace, Name: src.Name} - var data map[string]string - var err error - - switch src.Kind { - case "ConfigMap": - data, err = utils.GetConfigMapData(ctx, kube, nn) - case "Secret": - data, err = utils.GetSecretData(ctx, kube, nn) - default: - return nil, fmt.Errorf("unsupported prompt source kind %q (apiGroup=%q) for %q", src.Kind, src.ApiGroup, src.Name) - } + data, err := utils.GetConfigMapData(ctx, kube, nn) if err != nil { return nil, fmt.Errorf("failed to resolve prompt source %q: %w", src.Name, err) } @@ -64,47 +55,39 @@ func resolvePromptSources(ctx context.Context, kube client.Client, namespace str return lookup, nil } -// buildTemplateContext constructs the template context from an Agent resource. -func buildTemplateContext(agent *v1alpha2.Agent) PromptTemplateContext { - ctx := PromptTemplateContext{ +// buildTemplateContext constructs the template context from an Agent resource and its +// already-translated AgentConfig. Tool names are extracted from the config rather than +// recomputed from the spec. +func buildTemplateContext(agent *v1alpha2.Agent, cfg *adk.AgentConfig) PromptTemplateContext { + tplCtx := PromptTemplateContext{ AgentName: agent.Name, AgentNamespace: agent.Namespace, Description: agent.Spec.Description, } - // Collect tool names from all MCP server tools. - if agent.Spec.Declarative != nil { - for _, tool := range agent.Spec.Declarative.Tools { - if tool.McpServer != nil { - ctx.ToolNames = append(ctx.ToolNames, tool.McpServer.ToolNames...) - } - } + // Collect tool names from the already-translated agent config. + for _, t := range cfg.HttpTools { + tplCtx.ToolNames = append(tplCtx.ToolNames, t.Tools...) + } + for _, t := range cfg.SseTools { + tplCtx.ToolNames = append(tplCtx.ToolNames, t.Tools...) } - // Collect skill names from OCI refs and git refs. + // Collect skill names using the shared OCI/Git name helpers. if agent.Spec.Skills != nil { for _, ref := range agent.Spec.Skills.Refs { - // Use the last segment of the OCI reference as the skill name. - parts := strings.Split(ref, "/") - name := parts[len(parts)-1] - // Strip tag if present (e.g., "image:v1" -> "image"). - if idx := strings.Index(name, ":"); idx != -1 { - name = name[:idx] + if name := ociSkillName(ref); name != "" { + tplCtx.SkillNames = append(tplCtx.SkillNames, name) } - ctx.SkillNames = append(ctx.SkillNames, name) } for _, gitRef := range agent.Spec.Skills.GitRefs { - if gitRef.Name != "" { - ctx.SkillNames = append(ctx.SkillNames, gitRef.Name) - } else { - // Fall back to repo URL last segment. - parts := strings.Split(strings.TrimSuffix(gitRef.URL, ".git"), "/") - ctx.SkillNames = append(ctx.SkillNames, parts[len(parts)-1]) + if name := gitSkillName(gitRef); name != "" { + tplCtx.SkillNames = append(tplCtx.SkillNames, name) } } } - return ctx + return tplCtx } // executeSystemMessageTemplate parses and executes the system message as a Go text/template. diff --git a/go/internal/controller/translator/agent/template_test.go b/go/internal/controller/translator/agent/template_test.go index 377e01559..d9fdd19a1 100644 --- a/go/internal/controller/translator/agent/template_test.go +++ b/go/internal/controller/translator/agent/template_test.go @@ -8,6 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/pkg/adk" ) func TestExecuteSystemMessageTemplate(t *testing.T) { @@ -176,10 +177,11 @@ func TestBuildTemplateContext(t *testing.T) { tests := []struct { name string agent *v1alpha2.Agent + cfg *adk.AgentConfig wantCtx PromptTemplateContext }{ { - name: "full agent with tools and skills", + name: "tool names from config, skill names from spec", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{ Name: "my-agent", @@ -188,20 +190,7 @@ func TestBuildTemplateContext(t *testing.T) { Spec: v1alpha2.AgentSpec{ Type: v1alpha2.AgentType_Declarative, Description: "A helpful agent", - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Tools: []*v1alpha2.Tool{ - { - McpServer: &v1alpha2.McpServerTool{ - ToolNames: []string{"get-pods", "describe-pod"}, - }, - }, - { - McpServer: &v1alpha2.McpServerTool{ - ToolNames: []string{"helm-install"}, - }, - }, - }, - }, + Declarative: &v1alpha2.DeclarativeAgentSpec{}, Skills: &v1alpha2.SkillForAgent{ Refs: []string{"ghcr.io/org/skill-k8s:v1", "ghcr.io/org/skill-helm"}, GitRefs: []v1alpha2.GitRepo{ @@ -211,6 +200,12 @@ func TestBuildTemplateContext(t *testing.T) { }, }, }, + cfg: &adk.AgentConfig{ + HttpTools: []adk.HttpMcpServerConfig{ + {Tools: []string{"get-pods", "describe-pod"}}, + {Tools: []string{"helm-install"}}, + }, + }, wantCtx: PromptTemplateContext{ AgentName: "my-agent", AgentNamespace: "production", @@ -220,7 +215,7 @@ func TestBuildTemplateContext(t *testing.T) { }, }, { - name: "agent with skills using digests and git URLs with query/fragment", + name: "skills with OCI digests and git URLs with query/fragment", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{ Name: "my-agent", @@ -229,20 +224,7 @@ func TestBuildTemplateContext(t *testing.T) { Spec: v1alpha2.AgentSpec{ Type: v1alpha2.AgentType_Declarative, Description: "A helpful agent", - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Tools: []*v1alpha2.Tool{ - { - McpServer: &v1alpha2.McpServerTool{ - ToolNames: []string{"get-pods", "describe-pod"}, - }, - }, - { - McpServer: &v1alpha2.McpServerTool{ - ToolNames: []string{"helm-install"}, - }, - }, - }, - }, + Declarative: &v1alpha2.DeclarativeAgentSpec{}, Skills: &v1alpha2.SkillForAgent{ Refs: []string{ "ghcr.io/org/skill-k8s@sha256:abcdef0123456789", @@ -260,6 +242,12 @@ func TestBuildTemplateContext(t *testing.T) { }, }, }, + cfg: &adk.AgentConfig{ + HttpTools: []adk.HttpMcpServerConfig{ + {Tools: []string{"get-pods", "describe-pod"}}, + {Tools: []string{"helm-install"}}, + }, + }, wantCtx: PromptTemplateContext{ AgentName: "my-agent", AgentNamespace: "production", @@ -269,47 +257,49 @@ func TestBuildTemplateContext(t *testing.T) { }, }, { - name: "agent with no tools or skills", + name: "SSE tools included", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{ - Name: "simple-agent", + Name: "sse-agent", Namespace: "default", }, Spec: v1alpha2.AgentSpec{ Type: v1alpha2.AgentType_Declarative, - Description: "Simple", + Description: "SSE agent", Declarative: &v1alpha2.DeclarativeAgentSpec{}, }, }, + cfg: &adk.AgentConfig{ + SseTools: []adk.SseMcpServerConfig{ + {Tools: []string{"sse-tool-1", "sse-tool-2"}}, + }, + }, wantCtx: PromptTemplateContext{ - AgentName: "simple-agent", + AgentName: "sse-agent", AgentNamespace: "default", - Description: "Simple", - ToolNames: nil, + Description: "SSE agent", + ToolNames: []string{"sse-tool-1", "sse-tool-2"}, SkillNames: nil, }, }, { - name: "agent with agent-type tools (not MCP)", + name: "empty config", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{ - Name: "orchestrator", + Name: "simple-agent", Namespace: "default", }, Spec: v1alpha2.AgentSpec{ - Type: v1alpha2.AgentType_Declarative, - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Tools: []*v1alpha2.Tool{ - { - Agent: &v1alpha2.TypedLocalReference{Name: "sub-agent"}, - }, - }, - }, + Type: v1alpha2.AgentType_Declarative, + Description: "Simple", + Declarative: &v1alpha2.DeclarativeAgentSpec{}, }, }, + cfg: &adk.AgentConfig{}, wantCtx: PromptTemplateContext{ - AgentName: "orchestrator", + AgentName: "simple-agent", AgentNamespace: "default", + Description: "Simple", ToolNames: nil, SkillNames: nil, }, @@ -318,7 +308,7 @@ func TestBuildTemplateContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := buildTemplateContext(tt.agent) + got := buildTemplateContext(tt.agent, tt.cfg) assert.Equal(t, tt.wantCtx, got) }) } diff --git a/go/internal/database/service.go b/go/internal/database/service.go index 4ab1f68d2..d96b79366 100644 --- a/go/internal/database/service.go +++ b/go/internal/database/service.go @@ -82,7 +82,7 @@ func BuildWhereClause(clauses ...Clause) string { if idx > 0 { clausesStr.WriteString(" AND ") } - clausesStr.WriteString(fmt.Sprintf("%s = %v", clause.Key, clause.Value)) + fmt.Fprintf(&clausesStr, "%s = %v", clause.Key, clause.Value) } return clausesStr.String() } diff --git a/go/test/e2e/invoke_api_test.go b/go/test/e2e/invoke_api_test.go index 7495fe84a..27feb1cbe 100644 --- a/go/test/e2e/invoke_api_test.go +++ b/go/test/e2e/invoke_api_test.go @@ -477,7 +477,7 @@ func TestE2EInvokeInlineAgent(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ ApiGroup: "kagent.dev", Kind: "RemoteMCPServer", Name: "kagent-tool-server", @@ -517,7 +517,7 @@ func TestE2EInvokeInlineAgentWithStreaming(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ ApiGroup: "kagent.dev", Kind: "RemoteMCPServer", Name: "kagent-tool-server", @@ -578,7 +578,7 @@ func TestE2EInvokeDeclarativeAgentWithMcpServerTool(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ ApiGroup: "kagent.dev", Kind: "MCPServer", Name: mcpServer.Name, @@ -845,7 +845,7 @@ func TestE2EInvokeSTSIntegration(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ ApiGroup: "kagent.dev", Kind: "MCPServer", Name: mcpServer.Name, @@ -1054,7 +1054,7 @@ func TestE2EInvokeAgentWithPromptTemplate(t *testing.T) { { Type: v1alpha2.ToolProviderType_McpServer, McpServer: &v1alpha2.McpServerTool{ - TypedLocalReference: v1alpha2.TypedLocalReference{ + TypedReference: v1alpha2.TypedReference{ ApiGroup: "kagent.dev", Kind: "RemoteMCPServer", Name: "kagent-tool-server", diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index c2beb13d7..ccdad1a95 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -9949,12 +9949,11 @@ spec: properties: dataSources: description: |- - DataSources defines the ConfigMaps or Secrets whose keys can be included in the systemMessage + DataSources defines the ConfigMaps whose keys can be included in the systemMessage using Go template syntax, e.g. include("alias/key") or include("name/key"). items: description: |- - PromptSource references a Kubernetes resource whose keys are available as prompt fragments. - Currently supports ConfigMap and Secret resources (core API group). + PromptSource references a ConfigMap whose keys are available as prompt fragments. In systemMessage templates, use include("alias/key") (or include("name/key") if no alias is set) to insert the value of a specific key from this source. properties: @@ -9969,8 +9968,6 @@ spec: type: string name: type: string - namespace: - type: string required: - name type: object diff --git a/metadata_plan.md b/metadata_plan.md new file mode 100644 index 000000000..a4e4e7113 --- /dev/null +++ b/metadata_plan.md @@ -0,0 +1,235 @@ +# Plan: Remove `kagent_` Metadata Prefix and Adopt Upstream ADK Converters + +## Overview + +Replace kagent's custom A2A metadata prefix (`kagent_`) and converter implementations with upstream Google ADK (`google-adk`) equivalents. This is a **breaking change** — all persisted A2A task data will be invalidated and requires a major version bump. + +## PR Sequence + +Each PR is independently mergeable and deployable. + +--- + +### PR 1 — Structural alignment: subclass upstream executor (no behavior change) + +**Goal**: Get kagent's ADK executor structurally closer to upstream by subclassing it, while keeping all existing behavior. + +**Files to modify**: +- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py` + +**Changes**: +1. Change `A2aAgentExecutor` to subclass `google.adk.a2a.A2aAgentExecutor` instead of `a2a.server.agent_execution.AgentExecutor` +2. Pass kagent's existing converters via `A2aAgentExecutorConfig`: + - `a2a_part_converter` → kagent's `convert_a2a_part_to_genai_part` + - `gen_ai_part_converter` → kagent's `convert_genai_part_to_a2a_part` + - `request_converter` → kagent's converter (needs adapter since signature differs) + - `event_converter` → kagent's `convert_event_to_a2a_events` +3. Override `execute()` and/or `_handle_request()` to preserve: + - Per-request runner creation + cleanup (`runner.close()`) + - OpenTelemetry span attributes + - Ollama-specific error handling + - Partial event filtering + - Session naming from first message + - Request header forwarding to session state + - Invocation ID tracking in final metadata + +**Verification**: All existing tests pass. No change in wire protocol or metadata keys. + +**Risk**: Medium — the upstream base class is marked `@a2a_experimental`. Need to verify that overriding `_handle_request()` provides enough control, or if `execute()` needs to be overridden entirely (which would reduce the benefit of subclassing). + +**Mitigation**: If subclassing proves too constrained, fall back to composition (wrapping upstream executor) or skip this PR and proceed with PR 2+. + +--- + +### PR 2 — Deduplicate constants (no behavior change) + +**Goal**: Remove duplicate constant definitions from `kagent-core` that already exist in upstream. + +**Files to modify**: +- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` +- `python/packages/kagent-core/src/kagent/core/a2a/__init__.py` + +**Changes**: +1. Remove these constants from `_consts.py` (they're duplicates of upstream): + - `A2A_DATA_PART_METADATA_TYPE_KEY` + - `A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY` + - `A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL` + - `A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE` + - `A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT` + - `A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE` +2. Re-export them from upstream: `from google.adk.a2a.converters.part_converter import ...` +3. Update `__init__.py` exports to re-export from the new source + +**Verification**: All existing tests pass. grep for each constant to confirm no import breaks. + +**Risk**: Low — the constant values are identical (`"type"`, `"function_call"`, etc.). The only risk is if upstream renames or moves them in a future version. + +--- + +### PR 3 — Switch metadata prefix from `kagent_` to `adk_` (breaking change) + +**Goal**: Align all metadata keys with upstream's `adk_` prefix. + +**This is a major version bump. All persisted A2A task data will be invalidated.** + +**Files to modify**: + +*Python (prefix definition)*: +- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` — change `KAGENT_METADATA_KEY_PREFIX = "kagent_"` to `"adk_"`, or replace `get_kagent_metadata_key()` with imports of upstream's `_get_adk_metadata_key()` +- Rename `get_kagent_metadata_key` → `get_adk_metadata_key` (or alias) across all call sites +- Update `__init__.py` exports + +*Python (all call sites — find/replace `get_kagent_metadata_key` → `get_adk_metadata_key`)*: +- `kagent-adk/src/kagent/adk/converters/event_converter.py` +- `kagent-adk/src/kagent/adk/converters/part_converter.py` +- `kagent-adk/src/kagent/adk/_agent_executor.py` +- `kagent-openai/src/kagent/openai/_event_converter.py` +- `kagent-openai/src/kagent/openai/_agent_executor.py` +- `kagent-langgraph/src/kagent/langgraph/_converters.py` +- `kagent-langgraph/src/kagent/langgraph/_metadata_utils.py` +- `kagent-langgraph/src/kagent/langgraph/_executor.py` +- `kagent-crewai/src/kagent/crewai/_listeners.py` +- `kagent-core/src/kagent/core/a2a/_task_store.py` +- `kagent-core/src/kagent/core/a2a/_hitl.py` + +*Go*: +- `go/cli/internal/tui/chat.go` line 337 — `"kagent_type"` → `"adk_type"` + +*TypeScript/UI*: +- `ui/src/lib/messageHandlers.ts` — update `ADKMetadata` interface fields from `kagent_*` to `adk_*` +- `ui/src/components/chat/ChatMessage.tsx` — update metadata key references +- `ui/src/components/chat/ToolCallDisplay.tsx` — update metadata key references +- `ui/src/lib/__tests__/messageHandlers.test.ts` — update test fixtures + +*Note*: `ui/src/lib/userStore.ts` uses `kagent_user_id` as a localStorage key — this is NOT A2A metadata, it's a browser storage key. Can stay as-is or be renamed separately. + +**Verification**: +- All Python, Go, and UI tests pass +- Manual test: deploy to Kind cluster, send a message, verify metadata keys in task store use `adk_` prefix +- Verify UI displays agent name, tool calls, token stats correctly + +**Risk**: High — cross-cutting change across 3 languages. Must be done atomically in one PR to avoid mismatched prefixes between components. + +--- + +### PR 4 — Replace part_converter with upstream + +**Goal**: Delete kagent's custom part converter and use upstream's directly. + +**Files to delete**: +- `python/packages/kagent-adk/src/kagent/adk/converters/part_converter.py` + +**Files to modify**: +- `python/packages/kagent-adk/src/kagent/adk/converters/__init__.py` — remove local exports +- `python/packages/kagent-adk/src/kagent/adk/converters/event_converter.py` — update import to `from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part` +- `python/packages/kagent-adk/src/kagent/adk/converters/request_converter.py` — update import +- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py` — update import if needed +- Any other files importing from `kagent.adk.converters.part_converter` + +**Behavior differences to accept**: +- Upstream wraps unhandled DataParts in `` tags (more robust for round-trip). Kagent currently does `json.dumps(part.data)` as plain text. Upstream's behavior is better. + +**Verification**: All converter tests pass. Manual test with function calls and file parts. + +**Risk**: Low — the converters are nearly identical now that the prefix matches. + +--- + +### PR 5 — Replace request_converter with upstream + +**Goal**: Delete kagent's custom request converter and use upstream's `AgentRunRequest`. + +**Files to delete**: +- `python/packages/kagent-adk/src/kagent/adk/converters/request_converter.py` + +**Files to modify**: +- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py`: + - Import `convert_a2a_request_to_agent_run_request` and `AgentRunRequest` from upstream + - Update `_handle_request()` to consume `AgentRunRequest` fields instead of dict keys + - Handle streaming mode at the executor level (apply `StreamingMode` to `run_request.run_config` after conversion, or set it in config) + +**Key adaptation**: Upstream's request converter doesn't have a `stream` parameter. Streaming mode must be set by the executor on the `RunConfig` after conversion: +```python +run_request = convert_a2a_request_to_agent_run_request(context) +if self._stream: + run_request.run_config.streaming_mode = StreamingMode.SSE +``` + +**Verification**: Test both streaming and non-streaming modes. Verify user_id extraction from auth context works. + +**Risk**: Medium — the return type changes from `dict` to `AgentRunRequest`. All code that accesses `run_args["user_id"]` etc. must change to `run_request.user_id`. + +--- + +### PR 6 — Replace event_converter with upstream + thin wrapper + +**Goal**: Delete the bulk of kagent's event converter, keep only the kagent-specific error handling as a wrapper. + +**Files to modify**: +- `python/packages/kagent-adk/src/kagent/adk/converters/event_converter.py` — gut it down to a thin wrapper + +**What to keep**: +- `error_mappings.py` (kagent-specific value-add, no upstream equivalent) +- A wrapper function with this signature matching `AdkEventToA2AEventsConverter`: + ```python + def convert_event_to_a2a_events_with_error_handling( + event, invocation_context, task_id, context_id, part_converter + ) -> list[A2AEvent]: + ``` + That: + 1. Checks if `event.error_code` is `FinishReason.STOP` — if so, treats it as normal completion (upstream doesn't do this) + 2. Delegates to upstream's `convert_event_to_a2a_events()` for everything else + 3. Post-processes error events to substitute user-friendly messages from `error_mappings.py` + +**What to delete**: +- `_get_context_metadata()` — upstream handles this +- `_create_artifact_id()` — upstream handles this +- `_process_long_running_tool()` — upstream handles this +- `convert_event_to_a2a_message()` — upstream handles this +- `_create_status_update_event()` — upstream handles this +- `_serialize_metadata_value()` — upstream handles this + +**Verification**: Test error scenarios (STOP, MAX_TOKENS, SAFETY). Test streaming with partial events. Test long-running tool detection. + +**Risk**: Medium-High — the event converter is the most complex piece. Thorough testing needed to ensure upstream's handling matches kagent's for all edge cases. Suggest feature-flagging this (e.g., env var to fall back to old converter) during rollout. + +--- + +### PR 7 (optional) — Rename HITL constants + +**Goal**: Cosmetic cleanup of Python constant names. + +**Files to modify**: +- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` — rename `KAGENT_HITL_*` to `HITL_*` +- `python/packages/kagent-core/src/kagent/core/a2a/_hitl.py` — update references +- `python/packages/kagent-core/src/kagent/core/a2a/__init__.py` — update exports +- `python/packages/kagent-langgraph/src/kagent/langgraph/_executor.py` — update imports +- All HITL test files + +**Note**: The actual string values in the protocol are already unprefixed (`"decision_type"`, `"approve"`, etc.). This is purely renaming Python identifiers. + +**Risk**: Low — internal rename only, no protocol change. + +--- + +## Files That Can Be Fully Deleted (cumulative after all PRs) + +- `kagent-adk/src/kagent/adk/converters/part_converter.py` (PR 4) +- `kagent-adk/src/kagent/adk/converters/request_converter.py` (PR 5) +- Most of `kagent-adk/src/kagent/adk/converters/event_converter.py` (PR 6, reduced to thin wrapper) + +## Files That Must Stay + +- `kagent-adk/src/kagent/adk/converters/error_mappings.py` — kagent-specific +- `kagent-adk/src/kagent/adk/_agent_executor.py` — subclass of upstream with kagent overrides +- `kagent-core/src/kagent/core/a2a/_task_store.py` — kagent-specific persistence +- `kagent-core/src/kagent/core/a2a/_hitl.py` — kagent-specific tool approval UX +- `kagent-core/src/kagent/core/a2a/_task_result_aggregator.py` — verify if upstream's is compatible +- All non-ADK converters (`kagent-openai`, `kagent-langgraph`, `kagent-crewai`) — no upstream equivalent + +## Risks + +1. **Upstream `@a2a_experimental` decorator** — upstream's A2A module is experimental and may change. Tight coupling increases maintenance burden on ADK version bumps. +2. **Private API usage** — upstream's `_get_adk_metadata_key()` has underscore prefix (private). May need to define own wrapper or petition upstream to make it public. +3. **Event converter edge cases** — upstream may handle edge cases differently than kagent (e.g., STOP finish reason). Thorough regression testing is critical. +4. **Non-ADK frameworks** — `kagent-openai`, `kagent-langgraph`, `kagent-crewai` still need their own converters and will continue to use `get_adk_metadata_key()` for consistent prefix. diff --git a/plan.md b/plan.md new file mode 100644 index 000000000..8f7e2b41c --- /dev/null +++ b/plan.md @@ -0,0 +1,238 @@ +# Implementation Plan: Built-in Prompts / Prompt Templates (#1401) + +## Overview + +Add Go `text/template` support to the Agent CRD's `systemMessage` field, enabling composable prompt fragments (`{{include "name"}}`) and variable interpolation (`{{.AgentName}}`). Templates are resolved at reconciliation time by the controller. Built-in prompts are shipped as a Helm ConfigMap. + +--- + +## Step 1: CRD Changes + +**File:** `go/api/v1alpha2/agent_types.go` + +Add `PromptTemplates` field to `DeclarativeAgentSpec`: + +```go +type DeclarativeAgentSpec struct { + // ... existing fields ... + + // PromptTemplates defines named prompt fragments that can be referenced + // in systemMessage using Go template syntax: {{include "name"}}. + // When this field is non-empty, systemMessage is treated as a Go template + // with access to include() and agent context variables. + // +optional + PromptTemplates []PromptTemplateRef `json:"promptTemplates,omitempty"` +} + +type PromptTemplateRef struct { + // Name is the template identifier used in {{include "name"}} directives. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // ValueFrom specifies where the template content is loaded from. + // +kubebuilder:validation:Required + ValueFrom ValueSource `json:"valueFrom"` +} +``` + +Run `make -C go generate` after. + +--- + +## Step 2: Template Resolution in Translator + +**File:** `go/internal/controller/translator/agent/adk_api_translator.go` + +Modify `resolveSystemMessage()` to apply Go templating when `promptTemplates` is non-empty: + +``` +resolveSystemMessage(ctx, agent) + 1. Get raw system message (existing logic — inline or ValueSource) + 2. If agent.Spec.Declarative.PromptTemplates is empty → return raw string (backwards compat) + 3. Fetch all referenced template contents via ValueSource.Resolve() + 4. Build template context: + - .AgentName = agent.Name + - .AgentNamespace = agent.Namespace + - .Description = agent.Spec.Description + - .ToolNames = collected from agent.Spec.Declarative.Tools[*].McpServer.ToolNames + - .SkillNames = collected from agent.Spec.Skills.Refs + agent.Spec.Skills.GitRefs[*].Name + 5. Register custom "include" function that looks up from resolved templates map + 6. Parse and execute template + 7. Return resolved string (or error) +``` + +New helper types/functions to add (can be in a new file like `template.go` in the same package): + +```go +type PromptTemplateContext struct { + AgentName string + AgentNamespace string + Description string + ToolNames []string + SkillNames []string +} + +func resolvePromptTemplates(ctx context.Context, kube client.Client, namespace string, refs []v1alpha2.PromptTemplateRef) (map[string]string, error) + +func executeSystemMessageTemplate(rawMessage string, templates map[string]string, tplCtx PromptTemplateContext) (string, error) +``` + +--- + +## Step 3: ConfigMap Watch in Agent Controller + +**File:** `go/internal/controller/agent_controller.go` + +Add a ConfigMap watch following the existing pattern (ModelConfig, RemoteMCPServer, Service): + +```go +// In SetupWithManager, add: +Watches( + &corev1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(r.findAgentsReferencingConfigMap), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), +) +``` + +Implement `findAgentsReferencingConfigMap`: +- List all Agents in the ConfigMap's namespace +- For each Agent, check if any `promptTemplates[*].valueFrom` references this ConfigMap (type=ConfigMap, name matches) +- Also check if `systemMessageFrom` references this ConfigMap +- Return reconcile requests for matching agents + +--- + +## Step 4: Status Condition for Template Errors + +**File:** `go/api/v1alpha2/agent_types.go` + +No new condition type needed. Template resolution errors will cause reconciliation to fail, which already sets `Accepted=False` with `Reason=ReconcileFailed` and the error message. The existing pattern handles this. + +--- + +## Step 5: Helm Chart — Built-in Prompts ConfigMap + +**File:** `helm/kagent/templates/builtin-prompts-configmap.yaml` (new) + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: kagent-builtin-prompts + namespace: {{ .Release.Namespace }} +data: + skills-usage: | + # Skills Usage + You have access to skills — pre-built capabilities loaded from external sources. + Skills are available as files in your skills directory. ... + + tool-usage-best-practices: | + # Tool Usage Best Practices + When using tools, follow these guidelines: ... + + safety-guardrails: | + # Safety Guardrails + You must follow these safety guidelines: ... + + kubernetes-context: | + # Kubernetes Context + You are operating within a Kubernetes cluster. ... + + a2a-communication: | + # Agent-to-Agent Communication + You can communicate with other agents. ... +``` + +The actual prompt content will be written based on real agent usage patterns. These are placeholders for the structure. + +--- + +## Step 6: Example Usage + +After implementation, an Agent YAML would look like: + +```yaml +apiVersion: kagent.dev/v1alpha2 +kind: Agent +metadata: + name: my-agent + namespace: default +spec: + description: "A Kubernetes troubleshooting agent" + declarative: + modelConfig: default-model-config + systemMessage: | + {{include "skills-usage"}} + + You are {{.AgentName}}, a specialized agent for {{.Description}}. + + You have the following tools available: {{range .ToolNames}}{{.}}, {{end}} + + {{include "safety-guardrails"}} + promptTemplates: + - name: skills-usage + valueFrom: + type: ConfigMap + name: kagent-builtin-prompts + key: skills-usage + - name: safety-guardrails + valueFrom: + type: ConfigMap + name: kagent-builtin-prompts + key: safety-guardrails + tools: + - type: McpServer + mcpServer: + name: my-tool-server + kind: RemoteMCPServer + apiGroup: kagent.dev + toolNames: ["get-pods", "describe-pod"] +``` + +--- + +## Step 7: Tests + +### Unit Tests + +**File:** `go/internal/controller/translator/agent/template_test.go` (new) + +- Test `executeSystemMessageTemplate` with include directives +- Test variable interpolation (.AgentName, .ToolNames, etc.) +- Test backwards compatibility: no `promptTemplates` → raw string passthrough +- Test error cases: missing template name, invalid template syntax, missing ConfigMap +- Test that included content is NOT treated as a template (no nested includes) + +### E2E Test + +**File:** `go/test/e2e/` (add to existing test suite) + +- Create a ConfigMap with prompt content +- Create an Agent referencing it via `promptTemplates` +- Verify the resolved system message in the agent's config Secret +- Update the ConfigMap content → verify re-reconciliation produces updated prompt + +--- + +## Files Changed (Summary) + +| File | Change | +|------|--------| +| `go/api/v1alpha2/agent_types.go` | Add `PromptTemplates` field and `PromptTemplateRef` type | +| `go/internal/controller/translator/agent/adk_api_translator.go` | Modify `resolveSystemMessage()` to support templating | +| `go/internal/controller/translator/agent/template.go` | New — template resolution logic | +| `go/internal/controller/translator/agent/template_test.go` | New — unit tests | +| `go/internal/controller/agent_controller.go` | Add ConfigMap watch + `findAgentsReferencingConfigMap` | +| `helm/kagent/templates/builtin-prompts-configmap.yaml` | New — built-in prompt templates | +| `go/test/e2e/` | E2E test for prompt templates | + +--- + +## Non-Goals (Explicit) + +- No MCP prompts protocol integration +- No nested includes (templates from ConfigMaps are plain text) +- No new CRD (PromptTemplate kind) — ConfigMaps only for now +- No runtime resolution in Python ADK — controller handles everything +- No UI changes needed (resolved prompt is transparent to runtime) From e699394ed33521df3db1d0d407248a4f5520aeb1 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 14:13:39 +0000 Subject: [PATCH 08/14] update golang-ci version Signed-off-by: Eitan Yarmush --- go/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/Makefile b/go/Makefile index 9533342b1..da97404db 100644 --- a/go/Makefile +++ b/go/Makefile @@ -137,7 +137,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.19.0 ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v2.7.2 +GOLANGCI_LINT_VERSION ?= v2.10.1 .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. From 7f808afed2ba8537e3dcd35162db2e87476f32aa Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 14:38:23 +0000 Subject: [PATCH 09/14] fix: correct promptTemplate structure in 5 agent helm charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix k8s, helm, cilium-debug, cilium-manager, and cilium-policy agents that had the wrong YAML structure — using an array with configMapRef directly under promptTemplate instead of the correct dataSources object with kind: ConfigMap entries. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- helm/agents/cilium-debug/templates/agent.yaml | 5 +++-- helm/agents/cilium-manager/templates/agent.yaml | 5 +++-- helm/agents/cilium-policy/templates/agent.yaml | 5 +++-- helm/agents/helm/templates/agent.yaml | 5 +++-- helm/agents/k8s/templates/agent.yaml | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/helm/agents/cilium-debug/templates/agent.yaml b/helm/agents/cilium-debug/templates/agent.yaml index a3a85760f..8f6167bab 100644 --- a/helm/agents/cilium-debug/templates/agent.yaml +++ b/helm/agents/cilium-debug/templates/agent.yaml @@ -70,9 +70,10 @@ spec: - `cilium-dbg bpf recorder delete ` - Delete PCAP recorder promptTemplate: - - configMapRef: + dataSources: + - kind: ConfigMap name: kagent-builtin-prompts - alias: builtin + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/cilium-manager/templates/agent.yaml b/helm/agents/cilium-manager/templates/agent.yaml index 2a6ad65bc..5ea583c14 100644 --- a/helm/agents/cilium-manager/templates/agent.yaml +++ b/helm/agents/cilium-manager/templates/agent.yaml @@ -293,9 +293,10 @@ spec: - The `cilium monitor` tool is invaluable for real-time traffic analysis - For persistent issues, collect debug info with `cilium bugtool` promptTemplate: - - configMapRef: + dataSources: + - kind: ConfigMap name: kagent-builtin-prompts - alias: builtin + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/cilium-policy/templates/agent.yaml b/helm/agents/cilium-policy/templates/agent.yaml index 2097d7a22..4dfb4881a 100644 --- a/helm/agents/cilium-policy/templates/agent.yaml +++ b/helm/agents/cilium-policy/templates/agent.yaml @@ -453,9 +453,10 @@ spec: These tools can be used to troubleshoot policy issues, verify policy behavior, and ensure your network policies are correctly configured before deployment. promptTemplate: - - configMapRef: + dataSources: + - kind: ConfigMap name: kagent-builtin-prompts - alias: builtin + alias: builtin tools: - type: McpServer mcpServer: diff --git a/helm/agents/helm/templates/agent.yaml b/helm/agents/helm/templates/agent.yaml index 081b40648..0043da4eb 100644 --- a/helm/agents/helm/templates/agent.yaml +++ b/helm/agents/helm/templates/agent.yaml @@ -96,9 +96,10 @@ spec: Always prioritize stability and correctness in Helm operations, and provide clear guidance on how to verify the success of operations. promptTemplate: - - configMapRef: + dataSources: + - kind: ConfigMap name: kagent-builtin-prompts - alias: builtin + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer diff --git a/helm/agents/k8s/templates/agent.yaml b/helm/agents/k8s/templates/agent.yaml index 5e60a8830..8d2742cfe 100644 --- a/helm/agents/k8s/templates/agent.yaml +++ b/helm/agents/k8s/templates/agent.yaml @@ -82,9 +82,10 @@ spec: Always start with the least intrusive approach, and escalate diagnostics only as needed. When in doubt, gather more information before recommending changes. promptTemplate: - - configMapRef: + dataSources: + - kind: ConfigMap name: kagent-builtin-prompts - alias: builtin + alias: builtin modelConfig: {{ .Values.modelConfigRef | default (printf "%s" (include "kagent.defaultModelConfigName" .)) }} tools: - type: McpServer From 803695af1f893a7adaedd4a8902a4392e0e7aa42 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 09:38:57 -0500 Subject: [PATCH 10/14] Update helm/kagent/templates/builtin-prompts-configmap.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eitan Yarmush --- helm/kagent/templates/builtin-prompts-configmap.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/kagent/templates/builtin-prompts-configmap.yaml b/helm/kagent/templates/builtin-prompts-configmap.yaml index 0b13741e3..1cb12fe43 100644 --- a/helm/kagent/templates/builtin-prompts-configmap.yaml +++ b/helm/kagent/templates/builtin-prompts-configmap.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: name: kagent-builtin-prompts - namespace: {{ .Release.Namespace }} + namespace: {{ include "kagent.namespace" . }} labels: {{- include "kagent.labels" . | nindent 4 }} data: From 4927341893f27e135b7d000460774804fd8f9bc6 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 09:39:14 -0500 Subject: [PATCH 11/14] Update go/internal/controller/translator/agent/template.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eitan Yarmush --- go/internal/controller/translator/agent/template.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/internal/controller/translator/agent/template.go b/go/internal/controller/translator/agent/template.go index e3aafad3a..9ece7cc4f 100644 --- a/go/internal/controller/translator/agent/template.go +++ b/go/internal/controller/translator/agent/template.go @@ -48,6 +48,9 @@ func resolvePromptSources(ctx context.Context, kube client.Client, namespace str for key, value := range data { lookupKey := identifier + "/" + key + if _, exists := lookup[lookupKey]; exists { + return nil, fmt.Errorf("duplicate prompt template identifier %q from prompt source %q (kind=%q, apiGroup=%q)", lookupKey, src.Name, src.Kind, src.ApiGroup) + } lookup[lookupKey] = value } } From 0040c1c0bd87b1beaaaf0f7ee103b399023b2d88 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 14:43:14 +0000 Subject: [PATCH 12/14] oops Signed-off-by: Eitan Yarmush --- docs/architecture/prompt-templates.md | 38 +++++++++--------- .../controller/reconciler/file::memory: | Bin 204800 -> 0 bytes .../controller/reconciler/file::memory:-wal | 0 3 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 go/internal/controller/reconciler/file::memory: delete mode 100644 go/internal/controller/reconciler/file::memory:-wal diff --git a/docs/architecture/prompt-templates.md b/docs/architecture/prompt-templates.md index de597f7cf..5070c8779 100644 --- a/docs/architecture/prompt-templates.md +++ b/docs/architecture/prompt-templates.md @@ -4,9 +4,9 @@ This document explains the prompt templates feature — how it works, the design ## Overview -Prompt templates allow agents to compose their system messages from reusable fragments and dynamic variables, instead of writing everything from scratch. The system message field on the Agent CRD becomes a Go [text/template](https://pkg.go.dev/text/template) when prompt sources are configured, supporting two capabilities: +Prompt templates allow agents to compose their system messages from reusable fragments and dynamic variables, instead of writing everything from scratch. The system message field on the Agent CRD becomes a Go [text/template](https://pkg.go.dev/text/template) when a prompt template is configured, supporting two capabilities: -1. **Include directives** — `{{include "source/key"}}` inserts a prompt fragment from a ConfigMap or Secret +1. **Include directives** — `{{include "source/key"}}` inserts a prompt fragment from a ConfigMap 2. **Variable interpolation** — `{{.AgentName}}`, `{{.ToolNames}}`, etc. inject agent metadata Templates are resolved at **reconciliation time** by the controller. The final, fully-resolved prompt is baked into the agent's config Secret, so the Python ADK runtime receives a plain string with no template syntax. @@ -31,9 +31,9 @@ Templates are resolved at **reconciliation time** by the controller. The final, │ Controller (Go) │ │ │ │ 1. Resolve raw system message │ -│ 2. Fetch all data from referenced ConfigMaps/Secrets │ -│ 3. Build lookup map: "alias/key" → value │ -│ 4. Build template context from agent metadata │ +│ 2. Translate tools (MCP servers, agents) │ +│ 3. Fetch all data from referenced ConfigMaps │ +│ 4. Build template context from agent + translated config│ │ 5. Execute Go text/template with include + variables │ │ 6. Store resolved string in config Secret │ └──────────────────────┬───────────────────────────────────┘ @@ -79,11 +79,11 @@ Content pulled from ConfigMaps via `{{include "source/key"}}` is treated as **pl ### Why `TypedLocalReference` for prompt sources? -Each prompt source uses an inlined `TypedLocalReference` (`kind`, `apiGroup`, `name`) rather than a fixed enum like `type: ConfigMap`. This makes the API extensible — today it supports ConfigMaps and Secrets (core API group), but a future `PromptLibrary` CRD (`kind: PromptLibrary, apiGroup: kagent.dev`) could be added without changing the schema. +Each prompt source uses an inlined `TypedLocalReference` (`kind`, `apiGroup`, `name`) rather than a fixed enum. This makes the API extensible — today it supports ConfigMaps, but a future `PromptLibrary` CRD (`kind: PromptLibrary, apiGroup: kagent.dev`) could be added without changing the schema. The reference is local (same namespace as the agent) for simplicity and performance. -### Why ConfigMaps as the initial implementation? +### Why ConfigMaps only (no Secrets)? -ConfigMaps are a well-understood Kubernetes primitive and sufficient for the initial implementation. The `TypedLocalReference` pattern ensures the API is ready to support custom CRDs in the future without breaking changes. +Prompt templates contain prompt text, not sensitive credentials. Supporting Secrets would introduce unnecessary security risk — users might accidentally expose sensitive data in system prompts. ConfigMaps are the right primitive for non-sensitive configuration data. ### Backwards compatibility @@ -183,9 +183,9 @@ Kagent ships a `kagent-builtin-prompts` ConfigMap (deployed via Helm) with the f | Key | Description | |-----|-------------| | `skills-usage` | Instructions for agents that use skills from `/skills` | -| `tool-usage-best-practices` | Guidelines for effective tool usage | -| `safety-guardrails` | Safety rules for destructive operations and sensitive data | -| `kubernetes-context` | Context about operating within a Kubernetes cluster | +| `tool-usage-best-practices` | Guidelines for effective tool usage (read before write, explain before acting, verify after changes) | +| `safety-guardrails` | Safety rules: no destructive ops without confirmation, least privilege, rollback planning, protect sensitive data | +| `kubernetes-context` | Kubernetes operational methodology: investigation protocol, problem-solving framework, key principles | | `a2a-communication` | Guidelines for agent-to-agent communication | Example using built-in prompts: @@ -214,9 +214,11 @@ spec: alias: builtin ``` +All 10 shipped agent Helm charts (k8s, helm, cilium-debug, cilium-manager, cilium-policy, istio, kgateway, observability, promql, argo-rollouts) use the built-in prompts to replace their repeated safety, operational, and tool-usage sections. + ### Using multiple sources -You can reference multiple ConfigMaps and Secrets simultaneously: +You can reference multiple ConfigMaps simultaneously: ```yaml promptTemplate: @@ -227,9 +229,6 @@ promptTemplate: - kind: ConfigMap name: team-prompts alias: team - - kind: Secret - name: proprietary-prompts - alias: proprietary ``` Then use any key from any source: @@ -238,7 +237,6 @@ Then use any key from any source: systemMessage: | {{include "builtin/safety-guardrails"}} {{include "team/coding-standards"}} - {{include "proprietary/company-instructions"}} ``` ### Available template variables @@ -250,8 +248,10 @@ When `promptTemplate` is set, the following variables are available in `systemMe | `{{.AgentName}}` | `string` | `metadata.name` | | `{{.AgentNamespace}}` | `string` | `metadata.namespace` | | `{{.Description}}` | `string` | `spec.description` | -| `{{.ToolNames}}` | `[]string` | Collected from all `tools[*].mcpServer.toolNames` | -| `{{.SkillNames}}` | `[]string` | Derived from `skills.refs` (OCI image name) and `skills.gitRefs[*].name` | +| `{{.ToolNames}}` | `[]string` | Collected from the translated agent config (HTTP and SSE MCP tools) | +| `{{.SkillNames}}` | `[]string` | Derived from `skills.refs` and `skills.gitRefs` using shared OCI/Git name helpers | + +Template resolution happens **after** tools are translated, so `.ToolNames` reflects the actual tool names from the fully resolved MCP server configurations. You can use standard Go template constructs to work with these variables: @@ -285,6 +285,6 @@ kubectl get agent my-agent -o jsonpath='{.status.conditions[?(@.type=="Accepted" - [agent_types.go](../../go/api/v1alpha2/agent_types.go) — `PromptTemplateSpec`, `PromptSource` types - [template.go](../../go/internal/controller/translator/agent/template.go) — Template resolution logic - [template_test.go](../../go/internal/controller/translator/agent/template_test.go) — Unit tests -- [adk_api_translator.go](../../go/internal/controller/translator/agent/adk_api_translator.go) — `resolveSystemMessage()` integration +- [adk_api_translator.go](../../go/internal/controller/translator/agent/adk_api_translator.go) — Template integration in `translateInlineAgent()` - [agent_controller.go](../../go/internal/controller/agent_controller.go) — ConfigMap watch setup - [builtin-prompts-configmap.yaml](../../helm/kagent/templates/builtin-prompts-configmap.yaml) — Built-in prompt templates diff --git a/go/internal/controller/reconciler/file::memory: b/go/internal/controller/reconciler/file::memory: deleted file mode 100644 index 84d137c05978f935ec000736a747e8b3f3db61ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204800 zcmeI*&2Jm$oxpL9UO0NOEXRs6i4`TIxT!RiWtoy4=LJE^#*9~uEW3^b*trZDawJbB zkqkMqox?6dDcD~3(mfa0!=Bnh4@I$uc7gs0Ez)g)_P)IrSfB;kXNEJJXE;MvG}P>p z`xPvae4c0Kd7jVjnTIptGxA5@*|Z%?->uXR3y!{?x{w+gN_|t;Q>m09zSqQe@O4={ z8Trd!{^y#X;O2M#y82HKQsR45lv1Yu6P zPT$UcYcr?k!;HMXlrPq-f@77;f|J)v;+bO~T37V^QPq7BOD;sCv#X9>DK|4k`GZ2;F^iRQ*(!=UAF?u)8+LsY27D?Abn zHSJQc^^NYr-3Nnm}*0E!UR=r->v!tqC>{3-&I$oQUXB;oRplb8;$`_Zt!4&i& zc+{eUDo73nmR)wNJ*%e6@yqUP-`&^}+ug}+ZHvI2$(RX%Vsn7h7u|1-sMD^S)k@uV z?8jo-C?6eKHM^+a%6*XC+1%DY$Zmd=6NQ4#nC`q1pF!+;{m3$%&#L~+W89lfYs<^Z zLl4TIKI_(}M^?FLwY_{H!q%3#5;(1lOy{a^pTf?e)RDWcaD~`Qu7Ypv} zWjclWz6g3OV>TZBSeans<`{@&yJ7TqpEnvaGpcq+H0EZP#)K1AY-i7(raj^IU792n zGO`!b+RkdHSuxfKGZRaiX&gMKYOAZtiRwizRv?yeF%rFeEI%0i?e9kcWo(e1$sr)# zFAK3zDj#^>TyGe~3u3C$guXwU=v#$V+v*9PS`*6sMCb z2}9>?cA--xVY#+SqzuD2pVr>hI>o-(+Kl%w-PoN{HBD1amc1BziQbR%(O7zEo#H2T z&x?xTxRuI~_8=dP= zL0BqUfs}46PINubaeI?->zHi3H}0M19E*YPo_UHi_j@85Q8%emK>vP0k`D%dtRx(Y z5605k%AE3~HK7I$?mK}0d^}DmmufotDQ-W#lQv^tRkgR~l*V*y*I_LEjmA1x5SEQq zBV`#@S{&@6vhd_5XrVYC5i^Tj@?Wc+s|rfSDr#jIE2C*GJJ;!O530=gPtuJCBdRtx z7fkDZvi~p|9VC8gr*RUV2usIWFa?~+AZqop(_oP*7E}0e`|R4>J`9TYdL+)>UaraQ zrM4gF#!$D}wYj5=_b}YJp?I_F@rHLKdWqhTp(wMLCL@bpoe5uf7A9Z+iA%X%doQnl zw_YhgD(USKmY**5I_I{1Q0*~0R#{jRDlV%V_g3aYIx}v z0tg_000IagfB*srAbV){|7(D^bi3A5I_I{1Q0*~ z0R#|000FN583PC)fB*srAbwd400IagfB*srAbT>lS#jOif)2q1s} z0tg_000IagfB*ul|7Q#!fB*srAbfB*srAbn3qQtLNnQft@N-dMS|wz76}du{#gwVQ8e z)~?>X_QuUOZ@!Va{&6e!&F#!}k@fc4^{Y3o-&k9J^V<5GA3IjvS$$kMuuCh|O3hh0 zELQKlfA1r!_SmXj-7oA}W#?+idc3uobwB@Crhkhg0M3`u|Hq zU#6!2VS4`jPp5u2^`oiV=YD(c^U0r0et+`%#4pGHF#hxLkH`KvmR5hEuB3mE-Wt6; z^5cD0fTy2?`)eDbQQy|c23{ex|MsN=R0nZ*Y9rW`Ea|uzLXF5mp9g5No%(kl$2L*{nG>6 zvCP7eQ}JHQjmU)ic=dR6Uey*Bl#?B&@QG+!tEu6$SN!&EXTPb`LM8A+C!E0-=84z0ZISWg{&>+ZI`wX?Y?_OVy1997MI(foK`$*LD? zcGa;fyXu() zGa)olf6-*2FE_G17?qb)?VWk0G1Cghu4R=zDHQi5>LBS-JSahyRJs|MAfr7vyDzHR z4N;*Qukc7T)U->%mM?Xw%`Y0SD#$P@TE~tZTJ^eE+$2@?VwbAI((&4)JmYxj1y!4$ zSH8IH4W^(M!J`%(R6%kuuqnO9d{*^m z9^>9@T3cRL9(qvz^jWt)J+jJ0tL^0r5w^C>CHJ_yBDORd6VI#K?Nz0*+!|KZqxvJW zTygAOyI62%FViX1_eIcS8ME=|$I1j7H^)FM+YO_?`@GSZnNhVnqA@qSG$x#%V>^5P zH0=qu@6sfxkdeKZ)^=7q&5E%`n3-78Oyl4=Ra;$EPE;>)u>!G#i;?K%WBI}8Z+|}u zC}V^4Ob!9@ep!f(Qu)C1=6b^@UJz52CQnt?W3eRCtc&@>9Z$Ac8!28t7Y|o^xn_vG zRJY6F4)R8EI=PZCblzqcI#m*uYpX=cFpTqQ?Om-??3=C4cn{N!-6>VmH05O3i?NsJ z{Wu?urI*$zeo_a$5Q;gXLZ)}F8n(A(%iy@^O z=44_8p^dxIxegVCrJ@x`>Bizj*Yg~=HyO8%$;Nx*-g(Zk82Ij)r$}?ZC!!H`lR5?T z?-wNbVDQIE!lC$JEUm4~DNkAxYT)3$1NhI!T6V6};T}|( z@t>p{4@OjNZZ4SC{bc`PG&)H9)K23hJQ0?TwO|T3lR?z#Wv9U+RV=3P;r7|JxqTQE z@AXKWyS-eK+e>Xf(2b#Pvukrl8Si1ZaYOND*W(TENc0lDA45@QFHJ@ky*d-V@GMNe z{u7sSyY^mQ|8Bifz7jbH_1_Bl|Nl<;D+!7rfB*srAbyDq|EsyZiug2GI#J6j%-bmQmU1K4VgBA2fW9}QO_8rlR_u5+_*D0+M zOW5a1#};+iN7vScOI7kl&PZzy*E=ojNob0hNl3hMd~r!EO;E)A6m|%O34h@c}12Hg$I&+&q;50s5FPmDc0x)%@<3f!Ul^MauW@MX{9-d%2SUU ze0hNW-<(z)e}aG3p(>7vU|N6n(dfVTJnL{2<8N-U{SD<{&n!N&iu+Y@znEh_skwKL zh2coZ{YHFRN+>1mqd74pl!-)qZ^5_Vu_5)zG*>g(Pc%J-T#1|$?oNLq?cQ3)lU zIr5=yx!yL1wQgQ{?KO8zWmqz4wrj>l+$NY0Zi$hnJMov3j}mUC^OB|8UF1!Qj|$?Z zxr4Sgxigqpe4RMqp2aARE~xG_Z%P(IEruf!LJl#m7rv%y4;Pij-PVaxG=kMa&AnMd z+N)iPlvw;*qdI912_xGbC5c51>$0jXiYAUrO)OTu6N*SnzMj+)zhH+7rLc&%nAcP- zC+Z2$TqC=e4$MgU>q*thVBw_Dp%qdA}Nc5 z4TC$S#q`zNd1&$}?*G#}rTP57hu>sS6afSfKmY**5I_I{1Q0*~fj$-B`oB*DN3#(? z009ILKmY**5I_I{1Q6&!fb0JruqcWE0tg_000IagfB*srAb>!h3h?*;`!sMg8vz6m zKmY**5I_I{1Q0*~fgS|-{J#e*iXwmj0tg_000IagfB*srAke1*T>tlJ;Al1i2q1s} z0tg_000IagfB*tL2(bRY2P}#rfB*srAb Date: Sat, 28 Feb 2026 14:44:41 +0000 Subject: [PATCH 13/14] remove plans Signed-off-by: Eitan Yarmush --- metadata_plan.md | 235 ---------------------------------------------- plan.md | 238 ----------------------------------------------- 2 files changed, 473 deletions(-) delete mode 100644 metadata_plan.md delete mode 100644 plan.md diff --git a/metadata_plan.md b/metadata_plan.md deleted file mode 100644 index a4e4e7113..000000000 --- a/metadata_plan.md +++ /dev/null @@ -1,235 +0,0 @@ -# Plan: Remove `kagent_` Metadata Prefix and Adopt Upstream ADK Converters - -## Overview - -Replace kagent's custom A2A metadata prefix (`kagent_`) and converter implementations with upstream Google ADK (`google-adk`) equivalents. This is a **breaking change** — all persisted A2A task data will be invalidated and requires a major version bump. - -## PR Sequence - -Each PR is independently mergeable and deployable. - ---- - -### PR 1 — Structural alignment: subclass upstream executor (no behavior change) - -**Goal**: Get kagent's ADK executor structurally closer to upstream by subclassing it, while keeping all existing behavior. - -**Files to modify**: -- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py` - -**Changes**: -1. Change `A2aAgentExecutor` to subclass `google.adk.a2a.A2aAgentExecutor` instead of `a2a.server.agent_execution.AgentExecutor` -2. Pass kagent's existing converters via `A2aAgentExecutorConfig`: - - `a2a_part_converter` → kagent's `convert_a2a_part_to_genai_part` - - `gen_ai_part_converter` → kagent's `convert_genai_part_to_a2a_part` - - `request_converter` → kagent's converter (needs adapter since signature differs) - - `event_converter` → kagent's `convert_event_to_a2a_events` -3. Override `execute()` and/or `_handle_request()` to preserve: - - Per-request runner creation + cleanup (`runner.close()`) - - OpenTelemetry span attributes - - Ollama-specific error handling - - Partial event filtering - - Session naming from first message - - Request header forwarding to session state - - Invocation ID tracking in final metadata - -**Verification**: All existing tests pass. No change in wire protocol or metadata keys. - -**Risk**: Medium — the upstream base class is marked `@a2a_experimental`. Need to verify that overriding `_handle_request()` provides enough control, or if `execute()` needs to be overridden entirely (which would reduce the benefit of subclassing). - -**Mitigation**: If subclassing proves too constrained, fall back to composition (wrapping upstream executor) or skip this PR and proceed with PR 2+. - ---- - -### PR 2 — Deduplicate constants (no behavior change) - -**Goal**: Remove duplicate constant definitions from `kagent-core` that already exist in upstream. - -**Files to modify**: -- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` -- `python/packages/kagent-core/src/kagent/core/a2a/__init__.py` - -**Changes**: -1. Remove these constants from `_consts.py` (they're duplicates of upstream): - - `A2A_DATA_PART_METADATA_TYPE_KEY` - - `A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY` - - `A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL` - - `A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE` - - `A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT` - - `A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE` -2. Re-export them from upstream: `from google.adk.a2a.converters.part_converter import ...` -3. Update `__init__.py` exports to re-export from the new source - -**Verification**: All existing tests pass. grep for each constant to confirm no import breaks. - -**Risk**: Low — the constant values are identical (`"type"`, `"function_call"`, etc.). The only risk is if upstream renames or moves them in a future version. - ---- - -### PR 3 — Switch metadata prefix from `kagent_` to `adk_` (breaking change) - -**Goal**: Align all metadata keys with upstream's `adk_` prefix. - -**This is a major version bump. All persisted A2A task data will be invalidated.** - -**Files to modify**: - -*Python (prefix definition)*: -- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` — change `KAGENT_METADATA_KEY_PREFIX = "kagent_"` to `"adk_"`, or replace `get_kagent_metadata_key()` with imports of upstream's `_get_adk_metadata_key()` -- Rename `get_kagent_metadata_key` → `get_adk_metadata_key` (or alias) across all call sites -- Update `__init__.py` exports - -*Python (all call sites — find/replace `get_kagent_metadata_key` → `get_adk_metadata_key`)*: -- `kagent-adk/src/kagent/adk/converters/event_converter.py` -- `kagent-adk/src/kagent/adk/converters/part_converter.py` -- `kagent-adk/src/kagent/adk/_agent_executor.py` -- `kagent-openai/src/kagent/openai/_event_converter.py` -- `kagent-openai/src/kagent/openai/_agent_executor.py` -- `kagent-langgraph/src/kagent/langgraph/_converters.py` -- `kagent-langgraph/src/kagent/langgraph/_metadata_utils.py` -- `kagent-langgraph/src/kagent/langgraph/_executor.py` -- `kagent-crewai/src/kagent/crewai/_listeners.py` -- `kagent-core/src/kagent/core/a2a/_task_store.py` -- `kagent-core/src/kagent/core/a2a/_hitl.py` - -*Go*: -- `go/cli/internal/tui/chat.go` line 337 — `"kagent_type"` → `"adk_type"` - -*TypeScript/UI*: -- `ui/src/lib/messageHandlers.ts` — update `ADKMetadata` interface fields from `kagent_*` to `adk_*` -- `ui/src/components/chat/ChatMessage.tsx` — update metadata key references -- `ui/src/components/chat/ToolCallDisplay.tsx` — update metadata key references -- `ui/src/lib/__tests__/messageHandlers.test.ts` — update test fixtures - -*Note*: `ui/src/lib/userStore.ts` uses `kagent_user_id` as a localStorage key — this is NOT A2A metadata, it's a browser storage key. Can stay as-is or be renamed separately. - -**Verification**: -- All Python, Go, and UI tests pass -- Manual test: deploy to Kind cluster, send a message, verify metadata keys in task store use `adk_` prefix -- Verify UI displays agent name, tool calls, token stats correctly - -**Risk**: High — cross-cutting change across 3 languages. Must be done atomically in one PR to avoid mismatched prefixes between components. - ---- - -### PR 4 — Replace part_converter with upstream - -**Goal**: Delete kagent's custom part converter and use upstream's directly. - -**Files to delete**: -- `python/packages/kagent-adk/src/kagent/adk/converters/part_converter.py` - -**Files to modify**: -- `python/packages/kagent-adk/src/kagent/adk/converters/__init__.py` — remove local exports -- `python/packages/kagent-adk/src/kagent/adk/converters/event_converter.py` — update import to `from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part` -- `python/packages/kagent-adk/src/kagent/adk/converters/request_converter.py` — update import -- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py` — update import if needed -- Any other files importing from `kagent.adk.converters.part_converter` - -**Behavior differences to accept**: -- Upstream wraps unhandled DataParts in `` tags (more robust for round-trip). Kagent currently does `json.dumps(part.data)` as plain text. Upstream's behavior is better. - -**Verification**: All converter tests pass. Manual test with function calls and file parts. - -**Risk**: Low — the converters are nearly identical now that the prefix matches. - ---- - -### PR 5 — Replace request_converter with upstream - -**Goal**: Delete kagent's custom request converter and use upstream's `AgentRunRequest`. - -**Files to delete**: -- `python/packages/kagent-adk/src/kagent/adk/converters/request_converter.py` - -**Files to modify**: -- `python/packages/kagent-adk/src/kagent/adk/_agent_executor.py`: - - Import `convert_a2a_request_to_agent_run_request` and `AgentRunRequest` from upstream - - Update `_handle_request()` to consume `AgentRunRequest` fields instead of dict keys - - Handle streaming mode at the executor level (apply `StreamingMode` to `run_request.run_config` after conversion, or set it in config) - -**Key adaptation**: Upstream's request converter doesn't have a `stream` parameter. Streaming mode must be set by the executor on the `RunConfig` after conversion: -```python -run_request = convert_a2a_request_to_agent_run_request(context) -if self._stream: - run_request.run_config.streaming_mode = StreamingMode.SSE -``` - -**Verification**: Test both streaming and non-streaming modes. Verify user_id extraction from auth context works. - -**Risk**: Medium — the return type changes from `dict` to `AgentRunRequest`. All code that accesses `run_args["user_id"]` etc. must change to `run_request.user_id`. - ---- - -### PR 6 — Replace event_converter with upstream + thin wrapper - -**Goal**: Delete the bulk of kagent's event converter, keep only the kagent-specific error handling as a wrapper. - -**Files to modify**: -- `python/packages/kagent-adk/src/kagent/adk/converters/event_converter.py` — gut it down to a thin wrapper - -**What to keep**: -- `error_mappings.py` (kagent-specific value-add, no upstream equivalent) -- A wrapper function with this signature matching `AdkEventToA2AEventsConverter`: - ```python - def convert_event_to_a2a_events_with_error_handling( - event, invocation_context, task_id, context_id, part_converter - ) -> list[A2AEvent]: - ``` - That: - 1. Checks if `event.error_code` is `FinishReason.STOP` — if so, treats it as normal completion (upstream doesn't do this) - 2. Delegates to upstream's `convert_event_to_a2a_events()` for everything else - 3. Post-processes error events to substitute user-friendly messages from `error_mappings.py` - -**What to delete**: -- `_get_context_metadata()` — upstream handles this -- `_create_artifact_id()` — upstream handles this -- `_process_long_running_tool()` — upstream handles this -- `convert_event_to_a2a_message()` — upstream handles this -- `_create_status_update_event()` — upstream handles this -- `_serialize_metadata_value()` — upstream handles this - -**Verification**: Test error scenarios (STOP, MAX_TOKENS, SAFETY). Test streaming with partial events. Test long-running tool detection. - -**Risk**: Medium-High — the event converter is the most complex piece. Thorough testing needed to ensure upstream's handling matches kagent's for all edge cases. Suggest feature-flagging this (e.g., env var to fall back to old converter) during rollout. - ---- - -### PR 7 (optional) — Rename HITL constants - -**Goal**: Cosmetic cleanup of Python constant names. - -**Files to modify**: -- `python/packages/kagent-core/src/kagent/core/a2a/_consts.py` — rename `KAGENT_HITL_*` to `HITL_*` -- `python/packages/kagent-core/src/kagent/core/a2a/_hitl.py` — update references -- `python/packages/kagent-core/src/kagent/core/a2a/__init__.py` — update exports -- `python/packages/kagent-langgraph/src/kagent/langgraph/_executor.py` — update imports -- All HITL test files - -**Note**: The actual string values in the protocol are already unprefixed (`"decision_type"`, `"approve"`, etc.). This is purely renaming Python identifiers. - -**Risk**: Low — internal rename only, no protocol change. - ---- - -## Files That Can Be Fully Deleted (cumulative after all PRs) - -- `kagent-adk/src/kagent/adk/converters/part_converter.py` (PR 4) -- `kagent-adk/src/kagent/adk/converters/request_converter.py` (PR 5) -- Most of `kagent-adk/src/kagent/adk/converters/event_converter.py` (PR 6, reduced to thin wrapper) - -## Files That Must Stay - -- `kagent-adk/src/kagent/adk/converters/error_mappings.py` — kagent-specific -- `kagent-adk/src/kagent/adk/_agent_executor.py` — subclass of upstream with kagent overrides -- `kagent-core/src/kagent/core/a2a/_task_store.py` — kagent-specific persistence -- `kagent-core/src/kagent/core/a2a/_hitl.py` — kagent-specific tool approval UX -- `kagent-core/src/kagent/core/a2a/_task_result_aggregator.py` — verify if upstream's is compatible -- All non-ADK converters (`kagent-openai`, `kagent-langgraph`, `kagent-crewai`) — no upstream equivalent - -## Risks - -1. **Upstream `@a2a_experimental` decorator** — upstream's A2A module is experimental and may change. Tight coupling increases maintenance burden on ADK version bumps. -2. **Private API usage** — upstream's `_get_adk_metadata_key()` has underscore prefix (private). May need to define own wrapper or petition upstream to make it public. -3. **Event converter edge cases** — upstream may handle edge cases differently than kagent (e.g., STOP finish reason). Thorough regression testing is critical. -4. **Non-ADK frameworks** — `kagent-openai`, `kagent-langgraph`, `kagent-crewai` still need their own converters and will continue to use `get_adk_metadata_key()` for consistent prefix. diff --git a/plan.md b/plan.md deleted file mode 100644 index 8f7e2b41c..000000000 --- a/plan.md +++ /dev/null @@ -1,238 +0,0 @@ -# Implementation Plan: Built-in Prompts / Prompt Templates (#1401) - -## Overview - -Add Go `text/template` support to the Agent CRD's `systemMessage` field, enabling composable prompt fragments (`{{include "name"}}`) and variable interpolation (`{{.AgentName}}`). Templates are resolved at reconciliation time by the controller. Built-in prompts are shipped as a Helm ConfigMap. - ---- - -## Step 1: CRD Changes - -**File:** `go/api/v1alpha2/agent_types.go` - -Add `PromptTemplates` field to `DeclarativeAgentSpec`: - -```go -type DeclarativeAgentSpec struct { - // ... existing fields ... - - // PromptTemplates defines named prompt fragments that can be referenced - // in systemMessage using Go template syntax: {{include "name"}}. - // When this field is non-empty, systemMessage is treated as a Go template - // with access to include() and agent context variables. - // +optional - PromptTemplates []PromptTemplateRef `json:"promptTemplates,omitempty"` -} - -type PromptTemplateRef struct { - // Name is the template identifier used in {{include "name"}} directives. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // ValueFrom specifies where the template content is loaded from. - // +kubebuilder:validation:Required - ValueFrom ValueSource `json:"valueFrom"` -} -``` - -Run `make -C go generate` after. - ---- - -## Step 2: Template Resolution in Translator - -**File:** `go/internal/controller/translator/agent/adk_api_translator.go` - -Modify `resolveSystemMessage()` to apply Go templating when `promptTemplates` is non-empty: - -``` -resolveSystemMessage(ctx, agent) - 1. Get raw system message (existing logic — inline or ValueSource) - 2. If agent.Spec.Declarative.PromptTemplates is empty → return raw string (backwards compat) - 3. Fetch all referenced template contents via ValueSource.Resolve() - 4. Build template context: - - .AgentName = agent.Name - - .AgentNamespace = agent.Namespace - - .Description = agent.Spec.Description - - .ToolNames = collected from agent.Spec.Declarative.Tools[*].McpServer.ToolNames - - .SkillNames = collected from agent.Spec.Skills.Refs + agent.Spec.Skills.GitRefs[*].Name - 5. Register custom "include" function that looks up from resolved templates map - 6. Parse and execute template - 7. Return resolved string (or error) -``` - -New helper types/functions to add (can be in a new file like `template.go` in the same package): - -```go -type PromptTemplateContext struct { - AgentName string - AgentNamespace string - Description string - ToolNames []string - SkillNames []string -} - -func resolvePromptTemplates(ctx context.Context, kube client.Client, namespace string, refs []v1alpha2.PromptTemplateRef) (map[string]string, error) - -func executeSystemMessageTemplate(rawMessage string, templates map[string]string, tplCtx PromptTemplateContext) (string, error) -``` - ---- - -## Step 3: ConfigMap Watch in Agent Controller - -**File:** `go/internal/controller/agent_controller.go` - -Add a ConfigMap watch following the existing pattern (ModelConfig, RemoteMCPServer, Service): - -```go -// In SetupWithManager, add: -Watches( - &corev1.ConfigMap{}, - handler.EnqueueRequestsFromMapFunc(r.findAgentsReferencingConfigMap), - builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), -) -``` - -Implement `findAgentsReferencingConfigMap`: -- List all Agents in the ConfigMap's namespace -- For each Agent, check if any `promptTemplates[*].valueFrom` references this ConfigMap (type=ConfigMap, name matches) -- Also check if `systemMessageFrom` references this ConfigMap -- Return reconcile requests for matching agents - ---- - -## Step 4: Status Condition for Template Errors - -**File:** `go/api/v1alpha2/agent_types.go` - -No new condition type needed. Template resolution errors will cause reconciliation to fail, which already sets `Accepted=False` with `Reason=ReconcileFailed` and the error message. The existing pattern handles this. - ---- - -## Step 5: Helm Chart — Built-in Prompts ConfigMap - -**File:** `helm/kagent/templates/builtin-prompts-configmap.yaml` (new) - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: kagent-builtin-prompts - namespace: {{ .Release.Namespace }} -data: - skills-usage: | - # Skills Usage - You have access to skills — pre-built capabilities loaded from external sources. - Skills are available as files in your skills directory. ... - - tool-usage-best-practices: | - # Tool Usage Best Practices - When using tools, follow these guidelines: ... - - safety-guardrails: | - # Safety Guardrails - You must follow these safety guidelines: ... - - kubernetes-context: | - # Kubernetes Context - You are operating within a Kubernetes cluster. ... - - a2a-communication: | - # Agent-to-Agent Communication - You can communicate with other agents. ... -``` - -The actual prompt content will be written based on real agent usage patterns. These are placeholders for the structure. - ---- - -## Step 6: Example Usage - -After implementation, an Agent YAML would look like: - -```yaml -apiVersion: kagent.dev/v1alpha2 -kind: Agent -metadata: - name: my-agent - namespace: default -spec: - description: "A Kubernetes troubleshooting agent" - declarative: - modelConfig: default-model-config - systemMessage: | - {{include "skills-usage"}} - - You are {{.AgentName}}, a specialized agent for {{.Description}}. - - You have the following tools available: {{range .ToolNames}}{{.}}, {{end}} - - {{include "safety-guardrails"}} - promptTemplates: - - name: skills-usage - valueFrom: - type: ConfigMap - name: kagent-builtin-prompts - key: skills-usage - - name: safety-guardrails - valueFrom: - type: ConfigMap - name: kagent-builtin-prompts - key: safety-guardrails - tools: - - type: McpServer - mcpServer: - name: my-tool-server - kind: RemoteMCPServer - apiGroup: kagent.dev - toolNames: ["get-pods", "describe-pod"] -``` - ---- - -## Step 7: Tests - -### Unit Tests - -**File:** `go/internal/controller/translator/agent/template_test.go` (new) - -- Test `executeSystemMessageTemplate` with include directives -- Test variable interpolation (.AgentName, .ToolNames, etc.) -- Test backwards compatibility: no `promptTemplates` → raw string passthrough -- Test error cases: missing template name, invalid template syntax, missing ConfigMap -- Test that included content is NOT treated as a template (no nested includes) - -### E2E Test - -**File:** `go/test/e2e/` (add to existing test suite) - -- Create a ConfigMap with prompt content -- Create an Agent referencing it via `promptTemplates` -- Verify the resolved system message in the agent's config Secret -- Update the ConfigMap content → verify re-reconciliation produces updated prompt - ---- - -## Files Changed (Summary) - -| File | Change | -|------|--------| -| `go/api/v1alpha2/agent_types.go` | Add `PromptTemplates` field and `PromptTemplateRef` type | -| `go/internal/controller/translator/agent/adk_api_translator.go` | Modify `resolveSystemMessage()` to support templating | -| `go/internal/controller/translator/agent/template.go` | New — template resolution logic | -| `go/internal/controller/translator/agent/template_test.go` | New — unit tests | -| `go/internal/controller/agent_controller.go` | Add ConfigMap watch + `findAgentsReferencingConfigMap` | -| `helm/kagent/templates/builtin-prompts-configmap.yaml` | New — built-in prompt templates | -| `go/test/e2e/` | E2E test for prompt templates | - ---- - -## Non-Goals (Explicit) - -- No MCP prompts protocol integration -- No nested includes (templates from ConfigMaps are plain text) -- No new CRD (PromptTemplate kind) — ConfigMaps only for now -- No runtime resolution in Python ADK — controller handles everything -- No UI changes needed (resolved prompt is transparent to runtime) From ec4297d5980d1cda4ece44baaf8608b011f40b99 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sat, 28 Feb 2026 21:54:39 +0000 Subject: [PATCH 14/14] review comments and missing files Signed-off-by: Eitan Yarmush --- go/.gitignore | 3 + .../inputs/agent_with_prompt_template.yaml | 75 +++++ .../outputs/agent_with_prompt_template.json | 300 ++++++++++++++++++ go/internal/utils/secret.go | 13 - 4 files changed, 378 insertions(+), 13 deletions(-) create mode 100644 go/internal/controller/translator/agent/testdata/inputs/agent_with_prompt_template.yaml create mode 100644 go/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json diff --git a/go/.gitignore b/go/.gitignore index ada68ff08..2546b0497 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -25,3 +25,6 @@ go.work *.swp *.swo *~ + +# turso +file::* diff --git a/go/internal/controller/translator/agent/testdata/inputs/agent_with_prompt_template.yaml b/go/internal/controller/translator/agent/testdata/inputs/agent_with_prompt_template.yaml new file mode 100644 index 000000000..109e4b476 --- /dev/null +++ b/go/internal/controller/translator/agent/testdata/inputs/agent_with_prompt_template.yaml @@ -0,0 +1,75 @@ +operation: translateAgent +targetObject: agent-with-prompt-template +namespace: test +objects: + - apiVersion: v1 + kind: Secret + metadata: + name: openai-secret + namespace: test + data: + api-key: c2stdGVzdC1hcGkta2V5 # base64 encoded "sk-test-api-key" + - apiVersion: v1 + kind: ConfigMap + metadata: + name: my-prompts + namespace: test + data: + safety: | + ## Safety Guidelines + Never delete resources without explicit user confirmation. + preamble: | + ## Preamble + You are a helpful Kubernetes assistant. + - apiVersion: kagent.dev/v1alpha2 + kind: ModelConfig + metadata: + name: default-model + namespace: test + spec: + provider: OpenAI + model: gpt-4o + apiKeySecret: openai-secret + apiKeySecretKey: api-key + - apiVersion: kagent.dev/v1alpha2 + kind: RemoteMCPServer + metadata: + name: toolserver + namespace: test + spec: + url: http://localhost:8084/mcp + description: "KAgent Tool Server" + timeout: 30s + - apiVersion: kagent.dev/v1alpha2 + kind: Agent + metadata: + name: agent-with-prompt-template + namespace: test + spec: + type: Declarative + description: A Kubernetes troubleshooting agent + declarative: + systemMessage: | + {{include "builtin/preamble"}} + + You are {{.AgentName}}, operating in {{.AgentNamespace}}. + Your purpose: {{.Description}} + + Available tools: {{range .ToolNames}}{{.}}, {{end}} + + {{include "builtin/safety"}} + promptTemplate: + dataSources: + - kind: ConfigMap + name: my-prompts + alias: builtin + modelConfig: default-model + tools: + - type: McpServer + mcpServer: + name: toolserver + kind: RemoteMCPServer + apiGroup: kagent.dev + toolNames: + - k8s_get_resources + - k8s_describe_resource diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json new file mode 100644 index 000000000..d6ded43d3 --- /dev/null +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json @@ -0,0 +1,300 @@ +{ + "agentCard": { + "capabilities": { + "pushNotifications": false, + "stateTransitionHistory": true, + "streaming": true + }, + "defaultInputModes": [ + "text" + ], + "defaultOutputModes": [ + "text" + ], + "description": "A Kubernetes troubleshooting agent", + "name": "agent_with_prompt_template", + "skills": null, + "url": "http://agent-with-prompt-template.test:8080", + "version": "" + }, + "config": { + "description": "A Kubernetes troubleshooting agent", + "http_tools": [ + { + "params": { + "headers": {}, + "timeout": 30, + "url": "http://localhost:8084/mcp" + }, + "tools": [ + "k8s_get_resources", + "k8s_describe_resource" + ] + } + ], + "instruction": "## Preamble\nYou are a helpful Kubernetes assistant.\n\n\nYou are agent-with-prompt-template, operating in test.\nYour purpose: A Kubernetes troubleshooting agent\n\nAvailable tools: k8s_get_resources, k8s_describe_resource, \n\n## Safety Guidelines\nNever delete resources without explicit user confirmation.\n\n", + "model": { + "base_url": "", + "model": "gpt-4o", + "type": "openai" + }, + "remote_agents": null, + "sse_tools": null, + "stream": false + }, + "manifest": [ + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "agent-with-prompt-template", + "app.kubernetes.io/part-of": "kagent", + "kagent": "agent-with-prompt-template" + }, + "name": "agent-with-prompt-template", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "agent-with-prompt-template", + "uid": "" + } + ] + }, + "stringData": { + "agent-card.json": "{\"name\":\"agent_with_prompt_template\",\"description\":\"A Kubernetes troubleshooting agent\",\"url\":\"http://agent-with-prompt-template.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[]}", + "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"A Kubernetes troubleshooting agent\",\"instruction\":\"## Preamble\\nYou are a helpful Kubernetes assistant.\\n\\n\\nYou are agent-with-prompt-template, operating in test.\\nYour purpose: A Kubernetes troubleshooting agent\\n\\nAvailable tools: k8s_get_resources, k8s_describe_resource, \\n\\n## Safety Guidelines\\nNever delete resources without explicit user confirmation.\\n\\n\",\"http_tools\":[{\"params\":{\"url\":\"http://localhost:8084/mcp\",\"headers\":{},\"timeout\":30},\"tools\":[\"k8s_get_resources\",\"k8s_describe_resource\"]}],\"sse_tools\":null,\"remote_agents\":null,\"stream\":false}" + } + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "agent-with-prompt-template", + "app.kubernetes.io/part-of": "kagent", + "kagent": "agent-with-prompt-template" + }, + "name": "agent-with-prompt-template", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "agent-with-prompt-template", + "uid": "" + } + ] + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "agent-with-prompt-template", + "app.kubernetes.io/part-of": "kagent", + "kagent": "agent-with-prompt-template" + }, + "name": "agent-with-prompt-template", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "agent-with-prompt-template", + "uid": "" + } + ] + }, + "spec": { + "selector": { + "matchLabels": { + "app": "kagent", + "kagent": "agent-with-prompt-template" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": 1, + "maxUnavailable": 0 + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "annotations": { + "kagent.dev/config-hash": "12376619691294310081" + }, + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "agent-with-prompt-template", + "app.kubernetes.io/part-of": "kagent", + "kagent": "agent-with-prompt-template" + } + }, + "spec": { + "containers": [ + { + "args": [ + "--host", + "0.0.0.0", + "--port", + "8080", + "--filepath", + "/config" + ], + "env": [ + { + "name": "OPENAI_API_KEY", + "valueFrom": { + "secretKeyRef": { + "key": "api-key", + "name": "openai-secret" + } + } + }, + { + "name": "KAGENT_NAMESPACE", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.namespace" + } + } + }, + { + "name": "KAGENT_NAME", + "value": "agent-with-prompt-template" + }, + { + "name": "KAGENT_URL", + "value": "http://kagent-controller.kagent:8083" + } + ], + "image": "cr.kagent.dev/kagent-dev/kagent/app:dev", + "imagePullPolicy": "IfNotPresent", + "name": "kagent", + "ports": [ + { + "containerPort": 8080, + "name": "http" + } + ], + "readinessProbe": { + "httpGet": { + "path": "/.well-known/agent-card.json", + "port": "http" + }, + "initialDelaySeconds": 15, + "periodSeconds": 15, + "timeoutSeconds": 15 + }, + "resources": { + "limits": { + "cpu": "2", + "memory": "1Gi" + }, + "requests": { + "cpu": "100m", + "memory": "384Mi" + } + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config" + }, + { + "mountPath": "/var/run/secrets/tokens", + "name": "kagent-token" + } + ] + } + ], + "serviceAccountName": "agent-with-prompt-template", + "volumes": [ + { + "name": "config", + "secret": { + "secretName": "agent-with-prompt-template" + } + }, + { + "name": "kagent-token", + "projected": { + "sources": [ + { + "serviceAccountToken": { + "audience": "kagent", + "expirationSeconds": 3600, + "path": "kagent-token" + } + } + ] + } + } + ] + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "agent-with-prompt-template", + "app.kubernetes.io/part-of": "kagent", + "kagent": "agent-with-prompt-template" + }, + "name": "agent-with-prompt-template", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "agent-with-prompt-template", + "uid": "" + } + ] + }, + "spec": { + "ports": [ + { + "name": "http", + "port": 8080, + "targetPort": 8080 + } + ], + "selector": { + "app": "kagent", + "kagent": "agent-with-prompt-template" + }, + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + } + ] +} \ No newline at end of file diff --git a/go/internal/utils/secret.go b/go/internal/utils/secret.go index 782013e91..0fec44d73 100644 --- a/go/internal/utils/secret.go +++ b/go/internal/utils/secret.go @@ -8,19 +8,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// GetSecretData fetches all data from a Secret, converting byte values to strings. -func GetSecretData(ctx context.Context, c client.Client, ref client.ObjectKey) (map[string]string, error) { - secret := &corev1.Secret{} - if err := c.Get(ctx, ref, secret); err != nil { - return nil, fmt.Errorf("failed to find Secret %s: %v", ref.String(), err) - } - data := make(map[string]string, len(secret.Data)) - for k, v := range secret.Data { - data[k] = string(v) - } - return data, nil -} - // GetSecretValue fetches a value from a Secret func GetSecretValue(ctx context.Context, c client.Client, ref client.ObjectKey, key string) (string, error) { secret := &corev1.Secret{}