Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions go/api/adk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 12 additions & 0 deletions go/api/config/crd/bases/kagent.dev_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2472,6 +2472,18 @@ spec:
x-kubernetes-validations:
- message: selector must be specified when from is Selector
rule: '!(self.from == ''Selector'' && !has(self.selector))'
askUser:
description: |-
AskUser configures the "ask user" tool for this agent.
When enabled, the agent can pause execution and ask the user for input.
properties:
enabled:
description: Enabled indicates whether the "ask user" tool should
be enabled for this agent.
type: boolean
required:
- enabled
type: object
byo:
properties:
deployment:
Expand Down
12 changes: 12 additions & 0 deletions go/api/config/crd/bases/kagent.dev_sandboxagents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ spec:
x-kubernetes-validations:
- message: selector must be specified when from is Selector
rule: '!(self.from == ''Selector'' && !has(self.selector))'
askUser:
description: |-
AskUser configures the "ask user" tool for this agent.
When enabled, the agent can pause execution and ask the user for input.
properties:
enabled:
description: Enabled indicates whether the "ask user" tool should
be enabled for this agent.
type: boolean
required:
- enabled
type: object
byo:
properties:
deployment:
Expand Down
12 changes: 12 additions & 0 deletions go/api/v1alpha2/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ type AgentSpec struct {
// See: https://gateway-api.sigs.k8s.io/guides/multiple-ns/#cross-namespace-routing
// +optional
AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"`

// AskUser configures the "ask user" tool for this agent.
// When enabled, the agent can pause execution and ask the user for input.
// +optional
AskUser *AskUserSpec `json:"askUser,omitempty"`
}
Comment thread
dobesv marked this conversation as resolved.
Outdated

// AskUserSpec configures the "ask user" tool for an agent.
type AskUserSpec struct {
// Enabled indicates whether the "ask user" tool should be enabled for this agent.
// +kubebuilder:validation:Required
Enabled bool `json:"enabled"`
}

// +kubebuilder:validation:AtLeastOneOf=refs,gitRefs
Expand Down
20 changes: 20 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions go/core/internal/controller/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 spec.AskUser != nil && spec.AskUser.Enabled {
unsupported = append(unsupported, "ask user (not implemented in Go runtime)")
}

if len(unsupported) == 0 {
return ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(askUser *v1alpha2.AskUserSpec) *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",
},
AskUser: askUser,
},
}
}

tests := []struct {
name string
agent *v1alpha2.Agent
assertConfig func(t *testing.T, cfg *adk.AgentConfig)
}{
{
name: "ask user disabled",
agent: makeAgent(&v1alpha2.AskUserSpec{Enabled: 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.AskUserSpec{Enabled: true}),
assertConfig: func(t *testing.T, cfg *adk.AgentConfig) {
require.NotNil(t, cfg.AskUser)
assert.True(t, cfg.AskUser.Enabled)
},
},
{
name: "ask user 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)
}
})
}
}
6 changes: 6 additions & 0 deletions go/core/internal/controller/translator/agent/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent v1alp
Stream: new(spec.Declarative.Stream),
}

if spec.AskUser != nil {
cfg.AskUser = &adk.AskUserConfig{
Enabled: spec.AskUser.Enabled,
}
}

if spec.Sandbox != nil && spec.Sandbox.Network != nil {
cfg.Network = &adk.NetworkConfig{
AllowedDomains: append([]string(nil), spec.Sandbox.Network.AllowedDomains...),
Expand Down
12 changes: 12 additions & 0 deletions helm/kagent-crds/templates/kagent.dev_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2472,6 +2472,18 @@ spec:
x-kubernetes-validations:
- message: selector must be specified when from is Selector
rule: '!(self.from == ''Selector'' && !has(self.selector))'
askUser:
description: |-
AskUser configures the "ask user" tool for this agent.
When enabled, the agent can pause execution and ask the user for input.
properties:
enabled:
description: Enabled indicates whether the "ask user" tool should
be enabled for this agent.
type: boolean
required:
- enabled
type: object
byo:
properties:
deployment:
Expand Down
12 changes: 12 additions & 0 deletions helm/kagent-crds/templates/kagent.dev_sandboxagents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ spec:
x-kubernetes-validations:
- message: selector must be specified when from is Selector
rule: '!(self.from == ''Selector'' && !has(self.selector))'
askUser:
description: |-
AskUser configures the "ask user" tool for this agent.
When enabled, the agent can pause execution and ask the user for input.
properties:
enabled:
description: Enabled indicates whether the "ask user" tool should
be enabled for this agent.
type: boolean
required:
- enabled
type: object
byo:
properties:
deployment:
Expand Down
11 changes: 9 additions & 2 deletions python/packages/kagent-adk/src/kagent/adk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions python/packages/kagent-adk/tests/unittests/test_types.py
Original file line number Diff line number Diff line change
@@ -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",
Comment thread
dobesv marked this conversation as resolved.
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"
)
Loading