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: 5 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
--push
run: |
echo "Cache key: ${{ needs.setup.outputs.cache-key }}"
make build-controller build-golang-adk
make build-controller build-golang-adk build-claude-harness

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we should add -j here so we can parallelize these builds across the runner CPUs

make helm-install-provider
kubectl rollout status deployment/kagent-controller -n kagent --timeout=120s
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/component=controller -n kagent --timeout=120s
Expand All @@ -132,6 +132,9 @@ jobs:
RUNTIME_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/golang-adk:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }')
test -n "$RUNTIME_DIGEST"
export KAGENT_E2E_RUNTIME_IMAGE="localhost:5001/kagent-dev/kagent/golang-adk@${RUNTIME_DIGEST}"
CLAUDE_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/claude-harness:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, I don't want to end up in a situation like last release where the controller build was completely blocked on these dependencies

test -n "$CLAUDE_DIGEST"
export KAGENT_E2E_CLAUDE_IMAGE="localhost:5001/kagent-dev/kagent/claude-harness@${CLAUDE_DIGEST}"
envsubst < core/test/e2e/manifests/lifecycle.yaml.tmpl | kubectl apply -f -
for _ in $(seq 1 60); do
READY=$(kubectl get agenttemplate smoke -n kagent -o jsonpath='{.status.harnesses[?(@.harness=="kagent")].conditions[?(@.type=="Ready")].status}')
Expand Down Expand Up @@ -413,6 +416,7 @@ jobs:
- kagent-adk
- cli
- golang-adk
- claude-harness
runs-on: ubuntu-latest
services:
registry:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/image-scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- build_target: golang-adk
image_name: golang-adk
tag_suffix: ""
- build_target: claude-harness
image_name: claude-harness
tag_suffix: ""
runs-on: ubuntu-latest
services:
registry:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- ui
- kagent-adk
- golang-adk
- claude-harness
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ UI_IMAGE_NAME ?= ui
KAGENT_ADK_IMAGE_NAME ?= kagent-adk
GOLANG_ADK_IMAGE_NAME ?= golang-adk

CLAUDE_HARNESS_IMAGE_NAME ?= claude-harness
CONTROLLER_IMAGE_TAG ?= $(VERSION)
UI_IMAGE_TAG ?= $(VERSION)
KAGENT_ADK_IMAGE_TAG ?= $(VERSION)
GOLANG_ADK_IMAGE_TAG ?= $(VERSION)
CLAUDE_HARNESS_IMAGE_TAG ?= $(VERSION)
CONTROLLER_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(CONTROLLER_IMAGE_NAME):$(CONTROLLER_IMAGE_TAG)
UI_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(UI_IMAGE_NAME):$(UI_IMAGE_TAG)
KAGENT_ADK_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(KAGENT_ADK_IMAGE_NAME):$(KAGENT_ADK_IMAGE_TAG)
GOLANG_ADK_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(GOLANG_ADK_IMAGE_NAME):$(GOLANG_ADK_IMAGE_TAG)
CLAUDE_HARNESS_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(CLAUDE_HARNESS_IMAGE_NAME):$(CLAUDE_HARNESS_IMAGE_TAG)

#take from go/go.mod
AWK ?= $(shell command -v gawk || command -v awk)
Expand Down Expand Up @@ -219,17 +222,19 @@ build-all: ## Build all images for amd64+arm64 without pushing (outputs to /dev/
build-all: BUILD_ARGS ?= --progress=plain --builder $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --output type=tar,dest=/dev/null
build-all: proto-generate buildx-create
$(DOCKER_BUILDER) $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f go/Dockerfile ./go
$(DOCKER_BUILDER) $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f go/harness/claude/Dockerfile ./go
$(DOCKER_BUILDER) $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f ui/Dockerfile ./ui
$(DOCKER_BUILDER) $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f python/Dockerfile ./python

.PHONY: build
build: ## Build and push all component images
build: buildx-create build-ui build-kagent-adk build-golang-adk build-controller
build: buildx-create build-ui build-kagent-adk build-golang-adk build-claude-harness build-controller
@echo "Build completed successfully."
@echo "Controller Image: $(CONTROLLER_IMG)"
@echo "UI Image: $(UI_IMG)"
@echo "Kagent ADK Image: $(KAGENT_ADK_IMG)"
@echo "Golang ADK Image: $(GOLANG_ADK_IMG)"
@echo "Claude Harness Image: $(CLAUDE_HARNESS_IMG)"

.PHONY: build-monitor
build-monitor: ## Watch BuildKit process list inside the buildx container
Expand Down Expand Up @@ -257,6 +262,7 @@ build-img-versions: ## Print the fully-qualified image tags for all components
@echo ui=$(UI_IMG)
@echo kagent-adk=$(KAGENT_ADK_IMG)
@echo golang-adk=$(GOLANG_ADK_IMG)
@echo claude-harness=$(CLAUDE_HARNESS_IMG)

.PHONY: controller-manifests
controller-manifests: ## Regenerate CRD manifests and copy them into the Helm chart
Expand Down Expand Up @@ -289,6 +295,12 @@ build-golang-adk: proto-generate buildx-create
$(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --build-arg BUILD_PACKAGE=adk/cmd/main.go -t $(GOLANG_ADK_IMG) -f go/Dockerfile ./go
$(DOCKER_PUSH) $(GOLANG_ADK_IMG)

.PHONY: build-claude-harness
build-claude-harness: ## Build and push the native Claude Harness image
build-claude-harness: buildx-create
$(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(CLAUDE_HARNESS_IMG) -f go/harness/claude/Dockerfile ./go
$(DOCKER_PUSH) $(CLAUDE_HARNESS_IMG)

.PHONY: push
push: ## Push all component images (controller, ui, ADKs)
push: push-controller push-ui push-kagent-adk push-golang-adk
Expand Down
1 change: 1 addition & 0 deletions go/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
COPY api/ api/
COPY core/ core/
COPY adk/ adk/
COPY harness/ harness/

# Build
ARG LDFLAGS
Expand Down
15 changes: 12 additions & 3 deletions go/adk/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import (
runnerpkg "github.com/kagent-dev/kagent/go/adk/pkg/runner"
"github.com/kagent-dev/kagent/go/adk/pkg/session"
"github.com/kagent-dev/kagent/go/adk/pkg/telemetry"
"github.com/kagent-dev/kagent/go/core/v2/agentplugins"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const (
defaultPluginPackagesRoot = "/plugins"
defaultSkillsRoot = "/skills"
defaultPluginDataRoot = "/data/plugins"
)

func setupLogger(logLevel string) (logr.Logger, *zap.Logger) {
var zapLevel zapcore.Level
switch strings.ToLower(logLevel) {
Expand Down Expand Up @@ -93,9 +98,13 @@ func main() {
logger.Error(err, "Failed to load agent config (model configuration is required)", "configDir", configDir)
os.Exit(1)
}
if err := agentplugins.MaterializeAgentConfig(
if err := config.MaterializeAgentPlugins(
logr.NewContext(context.Background(), logger), agentConfig,
agentplugins.Paths{Plugins: agentplugins.DefaultPluginRoot, Skills: agentplugins.DefaultSkillsRoot, Data: agentplugins.DefaultDataRoot},
config.AgentPluginPaths{
Packages: defaultPluginPackagesRoot,
Skills: defaultSkillsRoot,
Data: defaultPluginDataRoot,
},
); err != nil {
logger.Error(err, "Failed to materialize Agent Plugins")
os.Exit(1)
Expand Down
70 changes: 70 additions & 0 deletions go/adk/pkg/config/agentplugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package config

import (
"context"
"fmt"
"path/filepath"

"github.com/kagent-dev/kagent/go/api/adk"
"github.com/kagent-dev/kagent/go/core/v2/agentplugins"
)

// AgentPluginPaths contains the ADK runtime destinations for Agent Plugin
// packages, selected skills, and mutable MCP server data.
type AgentPluginPaths struct {
Packages string
Skills string
Data string
}

// MaterializeAgentPlugins materializes plugins independently for each agent
// and adds their skills and MCP servers to the ADK runtime configuration.
func MaterializeAgentPlugins(ctx context.Context, agentConfig *adk.AgentConfig, paths AgentPluginPaths) error {
if agentConfig.AgentPlugins != nil {
materialization, err := agentplugins.Materialize(ctx, *agentConfig.AgentPlugins, agentplugins.Paths{
Packages: paths.Packages,
Skills: paths.Skills,
})
if err != nil {
return fmt.Errorf("materialize agent plugins: %w", err)
}
mcpConfig, err := agentplugins.LoadMCP(ctx, materialization, paths.Data)
if err != nil {
return fmt.Errorf("load agent plugin MCP configuration: %w", err)
}
addMCPConfig(agentConfig, mcpConfig)
agentConfig.SkillsDirectory = materialization.SkillsDirectory
}
for i, child := range agentConfig.SubAgents {
childRoot := filepath.Join("subagents", fmt.Sprintf("%d", i))
if err := MaterializeAgentPlugins(ctx, child, AgentPluginPaths{
Packages: filepath.Join(paths.Packages, childRoot),
Skills: filepath.Join(paths.Skills, childRoot),
Data: filepath.Join(paths.Data, childRoot),
}); err != nil {
return fmt.Errorf("materialize sub-agent %q: %w", child.Name, err)
}
}
return nil
}

func addMCPConfig(agentConfig *adk.AgentConfig, mcpConfig agentplugins.MCPConfig) {
for _, server := range mcpConfig.StreamableHTTP {
agentConfig.HttpTools = append(agentConfig.HttpTools, adk.HttpMcpServerConfig{
Params: adk.StreamableHTTPConnectionParams{Url: server.URL, Headers: server.Headers},
})
}
for _, server := range mcpConfig.SSE {
agentConfig.SseTools = append(agentConfig.SseTools, adk.SseMcpServerConfig{
Params: adk.SseConnectionParams{Url: server.URL, Headers: server.Headers},
})
}
for _, server := range mcpConfig.Stdio {
agentConfig.StdioTools = append(agentConfig.StdioTools, adk.StdioMcpServerConfig{
Command: server.Command,
Args: server.Args,
Env: server.Env,
Dir: server.Dir,
})
}
}
71 changes: 71 additions & 0 deletions go/adk/pkg/config/agentplugins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package config

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/kagent-dev/kagent/go/api/adk"
"github.com/kagent-dev/kagent/go/api/agentplugin"
"github.com/kagent-dev/kagent/go/core/v2/agentplugins"
)

func TestMaterializeAgentPluginsIsolatesSubagentSkills(t *testing.T) {
root := t.TempDir()
paths := AgentPluginPaths{
Packages: filepath.Join(root, "packages"),
Skills: filepath.Join(root, "skills"),
Data: filepath.Join(root, "data"),
}
source := agentplugin.Source{Git: &agentplugin.GitSource{URL: "unused", Commit: strings.Repeat("a", 40)}}
agentConfig := &adk.AgentConfig{
AgentPlugins: &agentplugin.Resources{Skills: []agentplugin.Skill{{Name: "root", Source: source}}},
SubAgents: []*adk.AgentConfig{{Name: "child", AgentPlugins: &agentplugin.Resources{Skills: []agentplugin.Skill{{Name: "child", Source: source}}}}},
}
for _, path := range []string{
filepath.Join(paths.Packages, "standalone-0"),
filepath.Join(paths.Packages, "subagents", "0", "standalone-0"),
} {
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "SKILL.md"), []byte("# Skill"), 0o644); err != nil {
t.Fatal(err)
}
}
if err := MaterializeAgentPlugins(context.Background(), agentConfig, paths); err != nil {
t.Fatal(err)
}
if agentConfig.SkillsDirectory == agentConfig.SubAgents[0].SkillsDirectory {
t.Fatalf("root and child share skills directory %q", agentConfig.SkillsDirectory)
}
for _, path := range []string{
filepath.Join(agentConfig.SkillsDirectory, "root", "SKILL.md"),
filepath.Join(agentConfig.SubAgents[0].SkillsDirectory, "child", "SKILL.md"),
} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("materialized skill %q: %v", path, err)
}
}
}

func TestAddMCPConfigConvertsRuntimeNeutralServers(t *testing.T) {
agentConfig := &adk.AgentConfig{}
addMCPConfig(agentConfig, agentplugins.MCPConfig{
StreamableHTTP: []agentplugins.RemoteMCPServer{{URL: "https://http.example.com", Headers: map[string]string{"X-Test": "http"}}},
SSE: []agentplugins.RemoteMCPServer{{URL: "https://sse.example.com", Headers: map[string]string{"X-Test": "sse"}}},
Stdio: []agentplugins.StdioMCPServer{{Command: "server", Args: []string{"--serve"}, Env: map[string]string{"KEY": "value"}, Dir: "/plugin"}},
})

if len(agentConfig.HttpTools) != 1 || agentConfig.HttpTools[0].Params.Url != "https://http.example.com" {
t.Fatalf("HTTP tools = %#v", agentConfig.HttpTools)
}
if len(agentConfig.SseTools) != 1 || agentConfig.SseTools[0].Params.Url != "https://sse.example.com" {
t.Fatalf("SSE tools = %#v", agentConfig.SseTools)
}
if len(agentConfig.StdioTools) != 1 || agentConfig.StdioTools[0].Command != "server" {
t.Fatalf("stdio tools = %#v", agentConfig.StdioTools)
}
}
45 changes: 4 additions & 41 deletions go/api/adk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"

"github.com/kagent-dev/kagent/go/api/agentplugin"
)

type StreamableHTTPConnectionParams struct {
Expand Down Expand Up @@ -548,45 +550,6 @@ type NetworkConfig struct {
AllowedDomains []string `json:"allowed_domains,omitempty"`
}

// AgentPluginConfig describes immutable Agent Plugin and standalone skill
// packages that the runtime must download before starting the agent.
type AgentPluginConfig struct {
Skills []StandaloneSkill `json:"skills,omitempty"`
Plugins []AgentPluginBundle `json:"plugins,omitempty"`
}

// StandaloneSkill identifies one independently sourced skill, rather than a
// skill selected from an Agent Plugin bundle.
type StandaloneSkill struct {
Name string `json:"name"`
Source AgentPluginSource `json:"source"`
}

type AgentPluginBundle struct {
Source AgentPluginSource `json:"source"`
Skills []string `json:"skills,omitempty"`
}

type AgentPluginSource struct {
OCI string `json:"oci,omitempty"`
Git *AgentPluginGit `json:"git,omitempty"`
S3 *AgentPluginS3 `json:"s3,omitempty"`
Path string `json:"path,omitempty"`
}

type AgentPluginGit struct {
URL string `json:"url"`
Commit string `json:"commit"`
}

type AgentPluginS3 struct {
Endpoint string `json:"endpoint"`
Bucket string `json:"bucket"`
Key string `json:"key"`
VersionID string `json:"versionId"`
Region string `json:"region,omitempty"`
}

// AgentContextConfig is the context management configuration that flows through config.json to the Python runtime.
type AgentContextConfig struct {
Compaction *AgentCompressionConfig `json:"compaction,omitempty"`
Expand Down Expand Up @@ -642,7 +605,7 @@ type AgentConfig struct {
Stream *bool `json:"stream,omitempty"`
Memory *MemoryConfig `json:"memory,omitempty"`
Network *NetworkConfig `json:"network,omitempty"`
AgentPlugins *AgentPluginConfig `json:"agent_plugins,omitempty"`
AgentPlugins *agentplugin.Resources `json:"agent_plugins,omitempty"`
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
ShareTools *bool `json:"share_tools,omitempty"`
SessionDBURL string `json:"session_db_url,omitempty"`
Expand Down Expand Up @@ -671,7 +634,7 @@ func (a *AgentConfig) UnmarshalJSON(data []byte) error {
Stream *bool `json:"stream,omitempty"`
Memory json.RawMessage `json:"memory"`
Network *NetworkConfig `json:"network,omitempty"`
AgentPlugins *AgentPluginConfig `json:"agent_plugins,omitempty"`
AgentPlugins *agentplugin.Resources `json:"agent_plugins,omitempty"`
ContextConfig *AgentContextConfig `json:"context_config,omitempty"`
ShareTools *bool `json:"share_tools,omitempty"`
SessionDBURL string `json:"session_db_url,omitempty"`
Expand Down
6 changes: 4 additions & 2 deletions go/api/adk/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"encoding/json"
"reflect"
"testing"

"github.com/kagent-dev/kagent/go/api/agentplugin"
)

func TestAgentConfigStdioToolsRoundTrip(t *testing.T) {
want := []StdioMcpServerConfig{{Command: "server", Args: []string{"--stdio"}, Env: map[string]string{"KEY": "value"}, Dir: "/plugin"}}
wantPlugins := &AgentPluginConfig{Skills: []StandaloneSkill{{
wantPlugins := &agentplugin.Resources{Skills: []agentplugin.Skill{{
Name: "review",
Source: AgentPluginSource{Git: &AgentPluginGit{
Source: agentplugin.Source{Git: &agentplugin.GitSource{
URL: "https://example.com/plugin.git", Commit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}},
}}}
Expand Down
Loading
Loading