diff --git a/go/api/adk/types.go b/go/api/adk/types.go index b9f939384..c412e54ce 100644 --- a/go/api/adk/types.go +++ b/go/api/adk/types.go @@ -448,6 +448,11 @@ func (c *AgentCompressionConfig) UnmarshalJSON(data []byte) error { return nil } +// AskUserConfig configures the "ask user" tool. +type AskUserConfig struct { + Enabled bool `json:"enabled"` +} + // See `python/packages/kagent-adk/src/kagent/adk/types.py` for the python version of this type AgentConfig struct { Model Model `json:"model"` @@ -461,6 +466,7 @@ type AgentConfig struct { Memory *MemoryConfig `json:"memory,omitempty"` Network *NetworkConfig `json:"network,omitempty"` ContextConfig *AgentContextConfig `json:"context_config,omitempty"` + AskUser *AskUserConfig `json:"ask_user,omitempty"` } // GetStream returns the stream value or default if not set @@ -492,6 +498,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error { Memory json.RawMessage `json:"memory"` Network *NetworkConfig `json:"network,omitempty"` ContextConfig *AgentContextConfig `json:"context_config,omitempty"` + AskUser *AskUserConfig `json:"ask_user,omitempty"` } if err := json.Unmarshal(data, &tmp); err != nil { return err @@ -521,6 +528,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error { a.Memory = memory a.Network = tmp.Network a.ContextConfig = tmp.ContextConfig + a.AskUser = tmp.AskUser return nil } diff --git a/go/api/config/crd/bases/kagent.dev_agents.yaml b/go/api/config/crd/bases/kagent.dev_agents.yaml index 8180fda2d..022069799 100644 --- a/go/api/config/crd/bases/kagent.dev_agents.yaml +++ b/go/api/config/crd/bases/kagent.dev_agents.yaml @@ -6200,6 +6200,16 @@ spec: minItems: 1 type: array type: object + builtinTools: + description: BuiltinTools configures the built-in tools available + to this agent. + properties: + askUser: + description: |- + AskUser enables the "ask user" tool. + When true, the agent can pause execution and ask the user for input. + type: boolean + type: object context: description: |- Context configures context management for this agent. diff --git a/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml b/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml index 9118e971b..93119a45f 100644 --- a/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml +++ b/go/api/config/crd/bases/kagent.dev_sandboxagents.yaml @@ -3850,6 +3850,16 @@ spec: minItems: 1 type: array type: object + builtinTools: + description: BuiltinTools configures the built-in tools available + to this agent. + properties: + askUser: + description: |- + AskUser enables the "ask user" tool. + When true, the agent can pause execution and ask the user for input. + type: boolean + type: object context: description: |- Context configures context management for this agent. diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index f19b0c3f4..c7a64aea9 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -84,6 +84,14 @@ type AgentSpec struct { AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"` } +// BuiltinToolsSpec configures the built-in tools available to a declarative agent. +type BuiltinToolsSpec struct { + // AskUser enables the "ask user" tool. + // When true, the agent can pause execution and ask the user for input. + // +optional + AskUser bool `json:"askUser,omitempty"` +} + // +kubebuilder:validation:AtLeastOneOf=refs,gitRefs type SkillForAgent struct { // Fetch images insecurely from registries (allowing HTTP and skipping TLS verification). @@ -209,6 +217,10 @@ type DeclarativeAgentSpec struct { // This includes event compaction (compression) and context caching. // +optional Context *ContextConfig `json:"context,omitempty"` + + // BuiltinTools configures the built-in tools available to this agent. + // +optional + BuiltinTools *BuiltinToolsSpec `json:"builtinTools,omitempty"` } // SandboxConfig configures sandboxed execution behavior. diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 9c5377240..8c4fb9aaa 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -335,6 +335,21 @@ func (in *BedrockConfig) DeepCopy() *BedrockConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BuiltinToolsSpec) DeepCopyInto(out *BuiltinToolsSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BuiltinToolsSpec. +func (in *BuiltinToolsSpec) DeepCopy() *BuiltinToolsSpec { + if in == nil { + return nil + } + out := new(BuiltinToolsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ByoDeploymentSpec) DeepCopyInto(out *ByoDeploymentSpec) { *out = *in @@ -495,6 +510,11 @@ func (in *DeclarativeAgentSpec) DeepCopyInto(out *DeclarativeAgentSpec) { *out = new(ContextConfig) (*in).DeepCopyInto(*out) } + if in.BuiltinTools != nil { + in, out := &in.BuiltinTools, &out.BuiltinTools + *out = new(BuiltinToolsSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeclarativeAgentSpec. diff --git a/go/core/internal/controller/reconciler/reconciler.go b/go/core/internal/controller/reconciler/reconciler.go index 212e8c431..fc1266624 100644 --- a/go/core/internal/controller/reconciler/reconciler.go +++ b/go/core/internal/controller/reconciler/reconciler.go @@ -807,6 +807,11 @@ func (a *kagentReconciler) validateRuntimeFeatures(agent v1alpha2.AgentObject) s unsupported = append(unsupported, "context compression/compaction (not implemented in Go runtime)") } + // AskUser: Not yet implemented in Go runtime + if decl.BuiltinTools != nil && decl.BuiltinTools.AskUser { + unsupported = append(unsupported, "ask user (not implemented in Go runtime)") + } + if len(unsupported) == 0 { return "" } diff --git a/go/core/internal/controller/translator/agent/adk_api_translator_test.go b/go/core/internal/controller/translator/agent/adk_api_translator_test.go index 6f0dbb80f..1ac34d6cd 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator_test.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator_test.go @@ -1413,3 +1413,84 @@ func Test_AdkApiTranslator_SandboxAgent_BYOEmitsSandbox(t *testing.T) { require.False(t, sawDeploy) require.False(t, sawService, "sandbox runtime must not include Service; agent-sandbox owns it") } + +func Test_AdkApiTranslator_AskUser(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-model", + Namespace: "default", + }, + Spec: v1alpha2.ModelConfigSpec{ + Model: "gpt-4", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + + makeAgent := func(builtinTools *v1alpha2.BuiltinToolsSpec) *v1alpha2.Agent { + return &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Description: "Test agent", + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "You are a test agent", + ModelConfig: "test-model", + BuiltinTools: builtinTools, + }, + }, + } + } + + tests := []struct { + name string + agent *v1alpha2.Agent + assertConfig func(t *testing.T, cfg *adk.AgentConfig) + }{ + { + name: "ask user disabled", + agent: makeAgent(&v1alpha2.BuiltinToolsSpec{AskUser: false}), + assertConfig: func(t *testing.T, cfg *adk.AgentConfig) { + require.NotNil(t, cfg.AskUser) + assert.False(t, cfg.AskUser.Enabled) + }, + }, + { + name: "ask user enabled", + agent: makeAgent(&v1alpha2.BuiltinToolsSpec{AskUser: true}), + assertConfig: func(t *testing.T, cfg *adk.AgentConfig) { + require.NotNil(t, cfg.AskUser) + assert.True(t, cfg.AskUser.Enabled) + }, + }, + { + name: "builtin tools not specified", + agent: makeAgent(nil), + assertConfig: func(t *testing.T, cfg *adk.AgentConfig) { + assert.Nil(t, cfg.AskUser) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(modelConfig.DeepCopy()). + Build() + + defaultModel := types.NamespacedName{Namespace: "default", Name: "test-model"} + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) + outputs, err := translator.TranslateAgent(context.Background(), trans, tt.agent) + + require.NoError(t, err) + require.NotNil(t, outputs) + require.NotNil(t, outputs.Config) + if tt.assertConfig != nil { + tt.assertConfig(t, outputs.Config) + } + }) + } +} diff --git a/go/core/internal/controller/translator/agent/compiler.go b/go/core/internal/controller/translator/agent/compiler.go index 0232a859e..e8e7d106a 100644 --- a/go/core/internal/controller/translator/agent/compiler.go +++ b/go/core/internal/controller/translator/agent/compiler.go @@ -176,6 +176,12 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent v1alp Stream: new(spec.Declarative.Stream), } + if spec.Declarative.BuiltinTools != nil { + cfg.AskUser = &adk.AskUserConfig{ + Enabled: spec.Declarative.BuiltinTools.AskUser, + } + } + if spec.Sandbox != nil && spec.Sandbox.Network != nil { cfg.Network = &adk.NetworkConfig{ AllowedDomains: append([]string(nil), spec.Sandbox.Network.AllowedDomains...), diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 8180fda2d..022069799 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -6200,6 +6200,16 @@ spec: minItems: 1 type: array type: object + builtinTools: + description: BuiltinTools configures the built-in tools available + to this agent. + properties: + askUser: + description: |- + AskUser enables the "ask user" tool. + When true, the agent can pause execution and ask the user for input. + type: boolean + type: object context: description: |- Context configures context management for this agent. diff --git a/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml b/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml index 9118e971b..93119a45f 100644 --- a/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml @@ -3850,6 +3850,16 @@ spec: minItems: 1 type: array type: object + builtinTools: + description: BuiltinTools configures the built-in tools available + to this agent. + properties: + askUser: + description: |- + AskUser enables the "ask user" tool. + When true, the agent can pause execution and ask the user for input. + type: boolean + type: object context: description: |- Context configures context management for this agent. diff --git a/python/packages/kagent-adk/src/kagent/adk/types.py b/python/packages/kagent-adk/src/kagent/adk/types.py index f31609b53..3baefc1dc 100644 --- a/python/packages/kagent-adk/src/kagent/adk/types.py +++ b/python/packages/kagent-adk/src/kagent/adk/types.py @@ -273,6 +273,12 @@ class NetworkConfig(BaseModel): allowed_domains: list[str] = Field(default_factory=list) +class AskUserConfig(BaseModel): + """Ask user tool configuration.""" + + enabled: bool + + class AgentConfig(BaseModel): model: ModelUnion = Field(discriminator="type") description: str @@ -285,6 +291,7 @@ class AgentConfig(BaseModel): memory: MemoryConfig | None = None # Memory configuration network: NetworkConfig | None = None context_config: ContextConfig | None = None + ask_user: AskUserConfig | None = None def to_agent(self, name: str, sts_integration: Optional[ADKTokenPropagationPlugin] = None) -> Agent: if name is None or not str(name).strip(): @@ -400,8 +407,8 @@ async def rewrite_url_to_proxy(request: httpx.Request) -> None: code_executor = SandboxedLocalCodeExecutor() if self.execute_code else None model = _create_llm_from_model_config(self.model) - # Add built-in ask_user tool unconditionally — every agent can ask the user questions. - tools.append(AskUserTool()) + if self.ask_user and self.ask_user.enabled: + tools.append(AskUserTool()) # Build before_tool_callback if any tools require approval before_tool_callback = make_approval_callback(tools_requiring_approval) if tools_requiring_approval else None diff --git a/python/packages/kagent-adk/tests/unittests/test_types.py b/python/packages/kagent-adk/tests/unittests/test_types.py new file mode 100644 index 000000000..6a973c4e6 --- /dev/null +++ b/python/packages/kagent-adk/tests/unittests/test_types.py @@ -0,0 +1,38 @@ +from kagent.adk.types import AgentConfig, AskUserConfig, GeminiVertexAI + + +def test_ask_user_enabled(): + """Verify that AskUserTool is added when ask_user.enabled is true.""" + config = AgentConfig( + model=GeminiVertexAI(model="gemini-pro", type="gemini_vertex_ai"), + description="Test Agent", + instruction="You are a test agent.", + ask_user=AskUserConfig(enabled=True), + ) + agent = config.to_agent(name="test_ask_user_enabled") + assert any(tool.name == "ask_user" for tool in agent.tools), "AskUserTool should be present when enabled" + + +def test_ask_user_disabled(): + """Verify that AskUserTool is not added when ask_user.enabled is false.""" + config = AgentConfig( + model=GeminiVertexAI(model="gemini-pro", type="gemini_vertex_ai"), + description="Test Agent", + instruction="You are a test agent.", + ask_user=AskUserConfig(enabled=False), + ) + agent = config.to_agent(name="test_ask_user_disabled") + assert not any(tool.name == "ask_user" for tool in agent.tools), "AskUserTool should not be present when disabled" + + +def test_ask_user_not_specified(): + """Verify that AskUserTool is not added when ask_user is not specified.""" + config = AgentConfig( + model=GeminiVertexAI(model="gemini-pro", type="gemini_vertex_ai"), + description="Test Agent", + instruction="You are a test agent.", + ) + agent = config.to_agent(name="test_ask_user_not_specified") + assert not any(tool.name == "ask_user" for tool in agent.tools), ( + "AskUserTool should not be present when not specified" + )