Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ jobs:
- app
- cli
- golang-adk
- golang-adk-full
- skills-init
runs-on: ubuntu-latest
services:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/image-scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
- app
- skills-init
- golang-adk
- golang-adk-full
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
- app
- golang-adk
- golang-adk-full
- skills-init
runs-on: ubuntu-latest
permissions:
Expand Down
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ UI_IMAGE_TAG ?= $(VERSION)
APP_IMAGE_TAG ?= $(VERSION)
KAGENT_ADK_IMAGE_TAG ?= $(VERSION)
GOLANG_ADK_IMAGE_TAG ?= $(VERSION)
GOLANG_ADK_FULL_IMAGE_TAG ?= $(VERSION)-full
SKILLS_INIT_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)
APP_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(APP_IMAGE_NAME):$(APP_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)
GOLANG_ADK_FULL_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(GOLANG_ADK_IMAGE_NAME):$(GOLANG_ADK_FULL_IMAGE_TAG)
SKILLS_INIT_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(SKILLS_INIT_IMAGE_NAME):$(SKILLS_INIT_IMAGE_TAG)

#take from go/go.mod
Expand Down Expand Up @@ -165,6 +167,7 @@ buildx-create:
build-all: BUILD_ARGS ?= --progress=plain --builder $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --output type=tar,dest=/dev/null
build-all: buildx-create
$(DOCKER_BUILDER) build $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f go/Dockerfile ./go
$(DOCKER_BUILDER) build $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f go/Dockerfile.full ./go
$(DOCKER_BUILDER) build $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f ui/Dockerfile ./ui
$(DOCKER_BUILDER) build $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f python/Dockerfile ./python

Expand Down Expand Up @@ -218,13 +221,14 @@ prune-docker-images:
docker images --filter dangling=true -q | xargs -r docker rmi || :

.PHONY: build
build: buildx-create build-controller build-ui build-app build-golang-adk build-skills-init
build: buildx-create build-controller build-ui build-app build-golang-adk build-golang-adk-full build-skills-init
@echo "Build completed successfully."
@echo "Controller Image: $(CONTROLLER_IMG)"
@echo "UI Image: $(UI_IMG)"
@echo "App Image: $(APP_IMG)"
@echo "Kagent ADK Image: $(KAGENT_ADK_IMG)"
@echo "Golang ADK Image: $(GOLANG_ADK_IMG)"
@echo "Golang ADK Full Image: $(GOLANG_ADK_FULL_IMG)"
@echo "Skills Init Image: $(SKILLS_INIT_IMG)"

.PHONY: build-monitor
Expand All @@ -246,6 +250,8 @@ build-img-versions:
@echo ui=$(UI_IMG)
@echo app=$(APP_IMG)
@echo kagent-adk=$(KAGENT_ADK_IMG)
@echo golang-adk=$(GOLANG_ADK_IMG)
@echo golang-adk-full=$(GOLANG_ADK_FULL_IMG)
@echo skills-init=$(SKILLS_INIT_IMG)

.PHONY: lint
Expand All @@ -254,7 +260,7 @@ lint:
make -C python lint

.PHONY: push
push: push-controller push-ui push-app push-kagent-adk push-golang-adk
push: push-controller push-ui push-app push-kagent-adk push-golang-adk push-golang-adk-full


.PHONY: controller-manifests
Expand Down Expand Up @@ -282,6 +288,10 @@ build-app: buildx-create build-kagent-adk
build-golang-adk: buildx-create
$(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --build-arg BUILD_PACKAGE=adk/cmd/main.go -t $(GOLANG_ADK_IMG) -f go/Dockerfile ./go

.PHONY: build-golang-adk-full
build-golang-adk-full: buildx-create
$(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --build-arg BUILD_PACKAGE=adk/cmd/main.go -t $(GOLANG_ADK_FULL_IMG) -f go/Dockerfile.full ./go

.PHONY: build-skills-init
build-skills-init: buildx-create
$(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) -t $(SKILLS_INIT_IMG) -f docker/skills-init/Dockerfile docker/skills-init
Expand Down
66 changes: 66 additions & 0 deletions go/Dockerfile.full
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
ARG BASE_IMAGE_REGISTRY=cgr.dev
ARG BUILDPLATFORM
FROM --platform=$BUILDPLATFORM $BASE_IMAGE_REGISTRY/chainguard/go:latest AS builder
ARG TARGETARCH
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG BUILD_PACKAGE=adk/cmd/main.go

WORKDIR /workspace
COPY go.mod go.sum .
RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
--mount=type=cache,target=/root/.cache/go-build,rw \
go mod download

COPY api/ api/
COPY core/ core/
COPY adk/ adk/

ARG LDFLAGS
RUN --mount=type=cache,target=/root/go/pkg/mod,rw \
--mount=type=cache,target=/root/.cache/go-build,rw \
echo "Building on $BUILDPLATFORM -> linux/$TARGETARCH" && \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags "$LDFLAGS" -o /app "$BUILD_PACKAGE"

FROM $BASE_IMAGE_REGISTRY/chainguard/wolfi-base:latest AS srt-builder
ARG TOOLS_PYTHON_VERSION=3.13

RUN --mount=type=cache,target=/var/cache/apk,rw \
apk add --no-cache \
bash git ca-certificates nodejs npm node-gyp bubblewrap python-${TOOLS_PYTHON_VERSION} libstdc++

RUN --mount=type=cache,target=/root/.npm \
mkdir -p /opt && \
cd /opt && \
git clone --depth 1 --revision=ef4afdef4d711ba21a507d7f7369e305f7d3dbfa https://github.com/anthropic-experimental/sandbox-runtime.git && \
cd sandbox-runtime && \
npm install && \
npm run build && \
npm prune --omit=dev

FROM $BASE_IMAGE_REGISTRY/chainguard/wolfi-base:latest
ARG TOOLS_PYTHON_VERSION=3.13

RUN --mount=type=cache,target=/var/cache/apk,rw \
apk add --no-cache \
bash ca-certificates curl nodejs bubblewrap socat python-${TOOLS_PYTHON_VERSION} ripgrep libstdc++

RUN addgroup -g 1001 goagent && \
adduser -u 1001 -G goagent -s /bin/bash -D goagent

COPY --from=srt-builder /opt/sandbox-runtime /opt/sandbox-runtime

RUN chmod +x /opt/sandbox-runtime/dist/cli.js && \
ln -s /opt/sandbox-runtime/dist/cli.js /usr/bin/srt

WORKDIR /
COPY --from=builder /app /app
ENV PATH="/usr/bin:/opt/sandbox-runtime/node_modules/.bin:$PATH"
ARG VERSION

LABEL org.opencontainers.image.source=https://github.com/kagent-dev/kagent
LABEL org.opencontainers.image.description="Kagent Go ADK runtime with sandbox execution dependencies."
LABEL org.opencontainers.image.version="$VERSION"

USER 1001:1001
ENTRYPOINT ["/app"]
1 change: 1 addition & 0 deletions go/adk/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ func main() {
Stream: stream,
AppName: appName,
Logger: logger,
RetryPolicy: agentConfig.RetryPolicy,
})

// Build the agent card.
Expand Down
63 changes: 62 additions & 1 deletion go/adk/pkg/a2a/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package a2a

import (
"context"
"errors"
"fmt"
"maps"
"os"
"time"

a2atype "github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2asrv"
Expand All @@ -13,6 +15,7 @@ import (
"github.com/kagent-dev/kagent/go/adk/pkg/session"
"github.com/kagent-dev/kagent/go/adk/pkg/skills"
"github.com/kagent-dev/kagent/go/adk/pkg/telemetry"
adkapi "github.com/kagent-dev/kagent/go/api/adk"
"go.opentelemetry.io/otel/attribute"
adkagent "google.golang.org/adk/agent"
"google.golang.org/adk/runner"
Expand All @@ -34,6 +37,7 @@ type KAgentExecutorConfig struct {
AppName string
SkillsDirectory string
Logger logr.Logger
RetryPolicy *adkapi.RetryPolicyConfig
}

// KAgentExecutor implements a2asrv.AgentExecutor
Expand All @@ -45,6 +49,7 @@ type KAgentExecutor struct {
appName string
skillsDirectory string
logger logr.Logger
retryPolicy *adkapi.RetryPolicyConfig
}

var _ a2asrv.AgentExecutor = (*KAgentExecutor)(nil)
Expand All @@ -66,6 +71,7 @@ func NewKAgentExecutor(cfg KAgentExecutorConfig) *KAgentExecutor {
appName: cfg.AppName,
skillsDirectory: skillsDir,
logger: cfg.Logger.WithName("kagent-executor"),
retryPolicy: cfg.RetryPolicy,
}
}

Expand Down Expand Up @@ -97,10 +103,65 @@ func (u *userIDInterceptor) Before(ctx context.Context, callCtx *a2asrv.CallCont
return ctx, nil
}

// computeRetryDelay calculates the delay for a retry attempt using exponential backoff.
func computeRetryDelay(attempt int, policy *adkapi.RetryPolicyConfig) time.Duration {
delay := policy.InitialRetryDelay * float64(int(1)<<attempt)
if policy.MaxRetryDelay != nil && delay > *policy.MaxRetryDelay {
delay = *policy.MaxRetryDelay
}
return time.Duration(delay * float64(time.Second))
}

// executeWithRetry runs fn with optional retry on failure.
// If policy is nil or MaxRetries is 0, fn is called once.
// Context cancellation is never retried.
func executeWithRetry(ctx context.Context, policy *adkapi.RetryPolicyConfig, fn func(ctx context.Context) error) error {
maxAttempts := 1
if policy != nil {
maxAttempts += policy.MaxRetries
}

var lastErr error
for attempt := range maxAttempts {
lastErr = fn(ctx)
if lastErr == nil {
return nil
}

// Never retry context cancellation
if ctx.Err() != nil || errors.Is(lastErr, context.Canceled) || errors.Is(lastErr, context.DeadlineExceeded) {
return lastErr
}

if attempt+1 < maxAttempts {
delay := computeRetryDelay(attempt, policy)
logr.FromContextOrDiscard(ctx).Info("Request failed, retrying",
"attempt", attempt+1,
"maxAttempts", maxAttempts,
"delay", delay,
"error", lastErr,
)
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
}
}
return lastErr
}

// Execute implements a2asrv.AgentExecutor.
func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error {
return executeWithRetry(ctx, e.retryPolicy, func(ctx context.Context) error {
return e.executeOnce(ctx, reqCtx, queue)
})
}

// executeOnce performs a single attempt at handling an A2A request.
// It follows the Python _handle_request pattern: set up session, handle HITL,
// convert inbound message, run the agent loop, and emit A2A events.
func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error {
func (e *KAgentExecutor) executeOnce(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error {
if reqCtx.Message == nil {
return fmt.Errorf("A2A request message cannot be nil")
}
Expand Down
Loading