Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
uv sync --all-extras
# Bump every package version to $VERSION first
uv version $VERSION --package kagent-core
uv version $VERSION --package kagent-skills
uv version $VERSION --package kagent-tools
uv version $VERSION --package kagent-adk
uv version $VERSION --package kagent-langgraph
uv version $VERSION --package kagent-crewai
Expand All @@ -115,13 +115,13 @@ jobs:
# Without this, a user upgrading e.g. kagent-adk via pip/uv could have
# older intra-repo siblings still locked at a stale version that may be
# missing symbols the new package requires.
for pkg in kagent-core kagent-skills agentsts-core agentsts-adk; do
for pkg in kagent-core kagent-tools agentsts-core agentsts-adk; do
find packages -name "pyproject.toml" \
-exec sed -i "s/\"${pkg}>=[^\"]*\"/\"${pkg}>=${VERSION}\"/g" {} +
done
# Build all packages
uv build --package kagent-core
uv build --package kagent-skills
uv build --package kagent-tools
uv build --package kagent-adk
uv build --package kagent-langgraph
uv build --package kagent-crewai
Expand Down
29 changes: 17 additions & 12 deletions docs/architecture/process-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ compiles `runtimeDefaults` into these payloads independently of its own settings
"log_level": "info",
"port": 80,
"max_content_length": "0",
"skill_tools": { "file_search_tools": false },
"builtin_tools": { "disable_file_search": false },
"token_propagation": { "enabled": false }
}
}
Expand Down Expand Up @@ -211,17 +211,22 @@ the same `TelemetryConfig` as controller settings (including protobuf durations)

An absent request size limit uses the runtime default; zero means unlimited.

Skill tool settings live together under `runtime.skill_tools`:
`file_search_tools`, `bash_venv_path`, and `disable_bash`. Move existing flat
settings into this block when updating configuration files.
Go ADK automatically registers the built-in `bash` tool when skills are loaded.
Set `runtime.skill_tools.disable_bash` to `true` to omit it for the root agent and all
subagents. Skill discovery and loading, `read_file`, `write_file`, and `edit_file`
remain available; `list_files` and `grep_file` still depend on
`runtime.skill_tools.file_search_tools`. For controller-managed runtimes, set
`config.runtimeDefaults.skillTools.disableBash: true` in Helm values. The default is `false`;
this setting affects only the built-in Go ADK bash tool, not Python or tools
provided through MCP or other integrations.
Built-in tool settings live under `runtime.builtin_tools`:
`disable_file_search`, `bash_venv_path`, and `disable_bash`. This replaces the
former `runtime.skill_tools` block (Helm: `config.runtimeDefaults.builtinTools`).
Replace the former `file_search_tools` flag with `disable_file_search`, reversing
its value: `file_search_tools: true` becomes `disable_file_search: false`.
Go and Python ADK register `read_file`, `write_file`, `edit_file`, `list_files`,
`grep_file`, and `bash` for the root agent and subagents, even when no skills are
configured or installed.
Set `runtime.builtin_tools.disable_bash` to `true` to omit built-in bash.
Set `runtime.builtin_tools.disable_file_search` to `true` to omit `list_files`
and `grep_file`. Both opt-outs default to `false`. Skill discovery and loading
remain conditional on installed skills.
For controller-managed runtimes, set
`config.runtimeDefaults.builtinTools.disableBash: true` in Helm values.
`bash_venv_path` selects a separate environment for shell commands in both runtimes:
its `bin` directory is prepended to `PATH`, and `VIRTUAL_ENV` is set.

Python integrations load this document with `KAgentConfig.from_file()` or receive
an explicit generated `RuntimeConfig`. Python ADK loads the full document with
Expand Down
17 changes: 17 additions & 0 deletions docs/architecture/runtime-and-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,23 @@ state there—local framework state, workspaces, and downloaded assets that must
survive Actor replacement. This state is runtime-private; public task history
remains in PostgreSQL.

The ADK's built-in file and shell tools use `/data/workspace`, shared by the root
agent and subagents in one Actor. Workspace files survive VOLUMES snapshots, resume,
and checkpoint forks. Skills are discovered independently; the workspace has no
session-specific directories or mutable skills symlinks. The workspace is created
on first use, with no predefined upload or output directories. File tools accept
normal relative and absolute paths inside the sandbox; Substrate owns isolation.

Each bash invocation starts a fresh shell with the Actor environment. The working
directory and configured skills directory are added to `PYTHONPATH`. Commands use
a 60-second default timeout; `timeout_seconds` can select up to 600 seconds.
Stdout and stderr are combined, captured up to 64 KiB, and returned with an
explicit truncation marker. Large results should be redirected to workspace files.
Commands and their process groups are stopped when a call finishes, times out,
or is canceled. Background processes are not durable work and have no process
handle API in the bash tool. Suspending an Actor preserves files, not running
commands. Command payloads and output are not logged by the shell helpers.

Substrate v0.6.0-alpha3 uses snapshot fidelity. Agent and standalone sandbox
templates use VOLUMES. Resume starts fresh containers
from the OCI image with the saved durable directories. Harnesses recover pending
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Shared types, interfaces, and implementations for the Kagent Go ADK.
- **models/** - LLM model adapters (OpenAI, Anthropic) implementing Google ADK's `model.LLM`
- **runner/** - Google ADK `runner.Config` creation from `AgentConfig` (agent, session service, memory, plugins, context compaction)
- **session/** - Session management, persistence, and ADK session service adapter
- **tools/** - Agent tools, including skill filesystem and shell execution
- **tools/** - Agent tools, including built-in filesystem and shell execution
- **taskstore/** - A2A task persistence through the kagent controller API
- **telemetry/** - OpenTelemetry tracing utilities

Expand Down
18 changes: 10 additions & 8 deletions go/adk/pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,28 @@ func createGoogleADKAgent(ctx context.Context, agentConfig *configv1.ADKAgentCon
}
toolsets := mcp.CreateToolsets(ctx, agentConfig.HttpTools, agentConfig.SseTools, agentConfig.StdioTools, propagateToken, dynamicHeaderProvider)
skillsDirectory := agentConfig.SkillsDirectory
builtinTools := runtime.GetBuiltinTools()
executionTools, err := tools.NewBuiltinTools(tools.BuiltinToolsConfig{
SkillsDirectory: skillsDirectory, BashVenvPath: builtinTools.GetBashVenvPath(),
DisableFileSearch: builtinTools.GetDisableFileSearch(), DisableBash: builtinTools.GetDisableBash(),
})
if err != nil {
return nil, fmt.Errorf("failed to create built-in tools: %w", err)
}
extraTools = append(extraTools, executionTools...)
if skillsDirectory != "" {
skillsSource := skill.NewFileSystemSource(os.DirFS(skillsDirectory))
skills, err := skillsSource.ListFrontmatters(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load skills: %w", err)
}
if len(skills) > 0 {
skillTools := runtime.GetSkillTools()
executionTools, err := tools.NewSkillExecutionTools(skillsDirectory, skillTools.GetFileSearchTools(), skillTools.GetDisableBash())
if err != nil {
return nil, fmt.Errorf("failed to create skill execution tools: %w", err)
}
extraTools = append(extraTools, executionTools...)

skillsToolset, err := skilltoolset.New(ctx, skilltoolset.Config{Source: skillsSource})
if err != nil {
return nil, fmt.Errorf("failed to create skill toolset: %w", err)
}
toolsets = append(toolsets, skillsToolset)
log.InfoContext(ctx, "wired local skills", "skills_directory", skillsDirectory, "skill_count", len(skills), "execution_tool_count", len(executionTools))
log.InfoContext(ctx, "wired local skills", "skills_directory", skillsDirectory, "skill_count", len(skills))
}
}
mcpAppToolNames := mcp.MCPAppToolNamesFromToolsets(toolsets)
Expand Down
136 changes: 75 additions & 61 deletions go/adk/pkg/agent/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,75 +22,89 @@ import (

// Exercise the runtime configuration through agent construction and the runner,
// inspecting the tools actually advertised to the model for each agent.
func TestSkillToolsRespectRuntimeDisableBash(t *testing.T) {
func TestBuiltinToolsIndependentOfSkills(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "test-key")
for _, disableBash := range []bool{false, true} {
for _, fileSearch := range []bool{false, true} {
t.Run(fmt.Sprintf("disable_bash=%t/file_search=%t", disableBash, fileSearch), func(t *testing.T) {
requests := make(chan []string, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request struct {
Tools []struct {
Function struct {
Name string `json:"name"`
} `json:"function"`
} `json:"tools"`
for _, skillsMode := range []string{"none", "empty", "installed"} {
for _, disableBash := range []bool{false, true} {
for _, disableFileSearch := range []bool{false, true} {
t.Run(fmt.Sprintf("skills=%s/disable_bash=%t/disable_file_search=%t", skillsMode, disableBash, disableFileSearch), func(t *testing.T) {
requests := make(chan []string, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request struct {
Tools []struct {
Function struct {
Name string `json:"name"`
} `json:"function"`
} `json:"tools"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Errorf("decode model request: %v", err)
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
var names []string
for _, tool := range request.Tools {
names = append(names, tool.Function.Name)
}
requests <- names
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"id":"test","object":"chat.completion","model":"test","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
if err != nil {
t.Errorf("write model response: %v", err)
}
}))
t.Cleanup(server.Close)

var skillsDir string
if skillsMode != "none" {
skillsDir = t.TempDir()
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Errorf("decode model request: %v", err)
http.Error(w, "invalid request", http.StatusBadRequest)
return
if skillsMode == "installed" {
require.NoError(t, os.Mkdir(filepath.Join(skillsDir, "demo"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(skillsDir, "demo", "SKILL.md"), []byte("---\nname: demo\ndescription: Demo skill.\n---\nRead files.\n"), 0644))
}
var names []string
for _, tool := range request.Tools {
names = append(names, tool.Function.Name)

model := &configv1.ADKModelConfig{Model: "gpt-4o", Provider: &configv1.ADKModelConfig_Openai{Openai: &configv1.ADKOpenAI{BaseUrl: server.URL}}}
cfg := &configv1.ADKConfig{
Runtime: &configv1.ADKRuntimeConfig{BuiltinTools: &configv1.BuiltinToolsConfig{DisableBash: disableBash, DisableFileSearch: disableFileSearch}},
Agent: &configv1.ADKAgentConfig{Model: model, SkillsDirectory: skillsDir, Stream: new(false),
SubAgents: []*configv1.ADKAgentConfig{{Name: "child", Model: model, SkillsDirectory: skillsDir, Stream: new(false)}},
},
}
requests <- names
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"id":"test","object":"chat.completion","model":"test","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
if err != nil {
t.Errorf("write model response: %v", err)
if !disableBash && !disableFileSearch {
cfg.Runtime = nil // Exercise absent tool configuration defaults.
}
}))
t.Cleanup(server.Close)

skillsDir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(skillsDir, "demo"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(skillsDir, "demo", "SKILL.md"), []byte("---\nname: demo\ndescription: Demo skill.\n---\nRead files.\n"), 0644))
model := &configv1.ADKModelConfig{Model: "gpt-4o", Provider: &configv1.ADKModelConfig_Openai{Openai: &configv1.ADKOpenAI{BaseUrl: server.URL}}}
cfg := &configv1.ADKConfig{
Runtime: &configv1.ADKRuntimeConfig{SkillTools: &configv1.SkillToolsConfig{DisableBash: disableBash, FileSearchTools: fileSearch}},
Agent: &configv1.ADKAgentConfig{Model: model, SkillsDirectory: skillsDir, Stream: new(false),
SubAgents: []*configv1.ADKAgentConfig{{Name: "child", Model: model, SkillsDirectory: skillsDir, Stream: new(false)}},
},
}
root, err := CreateGoogleADKAgent(t.Context(), cfg, "root", nil)
require.NoError(t, err)
for _, agent := range []adkagent.Agent{root, root.SubAgents()[0]} {
t.Run(agent.Name(), func(t *testing.T) {
sessions := adksession.InMemoryService()
r, err := runner.New(runner.Config{AppName: "test", Agent: agent, SessionService: sessions})
require.NoError(t, err)
sess, err := sessions.Create(t.Context(), &adksession.CreateRequest{AppName: "test", UserID: "user"})
require.NoError(t, err)
for _, err := range r.Run(t.Context(), "user", sess.Session.ID(), genai.NewContentFromText("List your tools.", genai.RoleUser), adkagent.RunConfig{}) {
root, err := CreateGoogleADKAgent(t.Context(), cfg, "root", nil)
require.NoError(t, err)
for _, agent := range []adkagent.Agent{root, root.SubAgents()[0]} {
t.Run(agent.Name(), func(t *testing.T) {
sessions := adksession.InMemoryService()
r, err := runner.New(runner.Config{AppName: "test", Agent: agent, SessionService: sessions})
require.NoError(t, err)
}
select {
case names := <-requests:
for _, name := range []string{"read_file", "write_file", "edit_file", "load_skill", "load_skill_resource", "list_skills"} {
require.Contains(t, names, name)
sess, err := sessions.Create(t.Context(), &adksession.CreateRequest{AppName: "test", UserID: "user"})
require.NoError(t, err)
for _, err := range r.Run(t.Context(), "user", sess.Session.ID(), genai.NewContentFromText("List your tools.", genai.RoleUser), adkagent.RunConfig{}) {
require.NoError(t, err)
}
require.Equal(t, !disableBash, slices.Contains(names, "bash"))
for _, name := range []string{"list_files", "grep_file"} {
require.Equal(t, fileSearch, slices.Contains(names, name), name)
select {
case names := <-requests:
for _, name := range []string{"read_file", "write_file", "edit_file"} {
require.Contains(t, names, name)
}
for _, name := range []string{"load_skill", "load_skill_resource", "list_skills"} {
require.Equal(t, skillsMode == "installed", slices.Contains(names, name), name)
}
require.Equal(t, !disableBash, slices.Contains(names, "bash"))
for _, name := range []string{"list_files", "grep_file"} {
require.Equal(t, !disableFileSearch, slices.Contains(names, name), name)
}
case <-time.After(time.Second):
t.Fatal("model received no request")
}
case <-time.After(time.Second):
t.Fatal("model received no request")
}
})
}
})
})
}
})
}
}
}
}
2 changes: 1 addition & 1 deletion go/adk/pkg/config/config_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
// - Used in create_agent() to add tools to the agent
//
// Agent.Spec.A2AConfig.Skills -> Not in config.json, handled separately
// - Skills are added via SkillsPlugin in Python
// - Python adds skill discovery alongside the built-in tools
// - In go-adk, skills are handled via AgentConfig.SkillsDirectory

// ValidateAgentConfigUsage validates that all AgentConfig fields are properly used
Expand Down
10 changes: 5 additions & 5 deletions go/adk/pkg/config/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func TestSharedRuntimeContract(t *testing.T) {
require.NotNil(t, cfg.MaxContentLength)
require.Zero(t, *cfg.MaxContentLength)
require.False(t, cfg.TokenPropagation.Enabled)
require.Equal(t, "/opt/venv", cfg.GetSkillTools().GetBashVenvPath())
require.False(t, cfg.GetSkillTools().GetFileSearchTools())
require.True(t, cfg.GetSkillTools().GetDisableBash())
require.Equal(t, "/opt/venv", cfg.GetBuiltinTools().GetBashVenvPath())
require.False(t, cfg.GetBuiltinTools().GetDisableFileSearch())
require.True(t, cfg.GetBuiltinTools().GetDisableBash())
require.False(t, cfg.Telemetry.CaptureContent)
require.Equal(t, int64(1500), cfg.Telemetry.Traces.Timeout.AsDuration().Milliseconds())
root := &configv1.ADKConfig{Runtime: &cfg, Agent: &configv1.ADKAgentConfig{SubAgents: []*configv1.ADKAgentConfig{{}}}}
Expand All @@ -35,8 +35,8 @@ func TestRuntimeDefaultsAndValidation(t *testing.T) {
var cfg configv1.ADKRuntimeConfig
require.NoError(t, protovalidate.Validate(&cfg))
require.Nil(t, cfg.MaxContentLength)
require.False(t, cfg.GetSkillTools().GetFileSearchTools())
require.False(t, cfg.GetSkillTools().GetDisableBash())
require.False(t, cfg.GetBuiltinTools().GetDisableFileSearch())
require.False(t, cfg.GetBuiltinTools().GetDisableBash())
for _, data := range []string{
`{"port":"0"}`, `{"max_content_length":-1}`, `{"control_plane_url":"file:///private"}`,
`{"token_propagation":{"resources":[""]}}`, `{"telemetry":{"max_capture_bytes":-1}}`,
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/config/testdata/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"tools"
]
},
"skill_tools": {
"builtin_tools": {
"bash_venv_path": "/opt/venv"
},
"telemetry": {
Expand Down
4 changes: 2 additions & 2 deletions go/adk/pkg/config/testdata/runtime.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"tools"
]
},
"skill_tools": {
"builtin_tools": {
"bash_venv_path": "/opt/venv",
"file_search_tools": false,
"disable_file_search": false,
"disable_bash": true
},
"telemetry": {
Expand Down
Loading
Loading