From 703c4993cf22a8d20070ec398b383727e7cf01b2 Mon Sep 17 00:00:00 2001 From: Yaniv Marom Nachumi Date: Sun, 5 Jul 2026 14:15:32 +0300 Subject: [PATCH 1/2] chore: upgrade google.golang.org/adk to v2.0.0 Bump the Go ADK dependency from v1.4.0 to v2.0.0. The major version requires the /v2 module path suffix, so all imports were rewritten accordingly. Adapts to the v2 breaking change that unified agent.ToolContext and agent.CallbackContext into a single agent.Context, and adds the new InvocationContext methods (IsolationScope, ResumedInput, WithICDelta) to the sts test fake. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Yaniv Marom Nachumi --- go/adk/examples/byo/main.go | 12 ++++++------ go/adk/examples/oneshot/main.go | 6 +++--- go/adk/pkg/a2a/agentcard.go | 4 ++-- go/adk/pkg/a2a/consts.go | 2 +- go/adk/pkg/a2a/converter.go | 4 ++-- go/adk/pkg/a2a/converter_test.go | 2 +- go/adk/pkg/a2a/executor.go | 6 +++--- go/adk/pkg/a2a/hitl.go | 2 +- go/adk/pkg/agent/agent.go | 20 ++++++++++---------- go/adk/pkg/agent/approval.go | 12 ++++++------ go/adk/pkg/agent/createllm_test.go | 6 +++--- go/adk/pkg/app/app.go | 2 +- go/adk/pkg/mcp/registry.go | 4 ++-- go/adk/pkg/memory/kagent_service.go | 6 +++--- go/adk/pkg/memory/kagent_service_test.go | 4 ++-- go/adk/pkg/memory/save_memory_tool.go | 8 ++++---- go/adk/pkg/models/anthropic_adk.go | 2 +- go/adk/pkg/models/bedrock.go | 2 +- go/adk/pkg/models/ollama_adk.go | 2 +- go/adk/pkg/models/openai_adk.go | 2 +- go/adk/pkg/models/sapaicore_adk.go | 2 +- go/adk/pkg/models/sapaicore_adk_test.go | 2 +- go/adk/pkg/runner/adapter.go | 10 +++++----- go/adk/pkg/session/local_session.go | 2 +- go/adk/pkg/session/session.go | 2 +- go/adk/pkg/session/session_test.go | 2 +- go/adk/pkg/sts/plugin.go | 4 ++-- go/adk/pkg/sts/plugin_test.go | 9 +++++++-- go/adk/pkg/telemetry/attributes.go | 2 +- go/adk/pkg/telemetry/attributes_test.go | 2 +- go/adk/pkg/telemetry/tracing.go | 2 +- go/adk/pkg/tools/ask_user.go | 8 ++++---- go/adk/pkg/tools/remote_a2a_tool.go | 18 +++++++++--------- go/adk/pkg/tools/share_tools.go | 12 ++++++------ go/adk/pkg/tools/skills.go | 16 ++++++++-------- go/go.mod | 2 +- go/go.sum | 4 ++-- 37 files changed, 106 insertions(+), 101 deletions(-) diff --git a/go/adk/examples/byo/main.go b/go/adk/examples/byo/main.go index 255f1a3d21..c958c1944d 100644 --- a/go/adk/examples/byo/main.go +++ b/go/adk/examples/byo/main.go @@ -46,12 +46,12 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/app" "github.com/kagent-dev/kagent/go/adk/pkg/models" "go.uber.org/zap" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/agent/workflowagents/parallelagent" - "google.golang.org/adk/runner" - "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. - adksession "google.golang.org/adk/session" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/agent/workflowagents/parallelagent" + "google.golang.org/adk/v2/runner" + "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. + adksession "google.golang.org/adk/v2/session" ) func main() { diff --git a/go/adk/examples/oneshot/main.go b/go/adk/examples/oneshot/main.go index 161b8340c7..7dc6642058 100644 --- a/go/adk/examples/oneshot/main.go +++ b/go/adk/examples/oneshot/main.go @@ -26,9 +26,9 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/config" "go.uber.org/zap" "go.uber.org/zap/zapcore" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/runner" - adksession "google.golang.org/adk/session" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/runner" + adksession "google.golang.org/adk/v2/session" "google.golang.org/genai" ) diff --git a/go/adk/pkg/a2a/agentcard.go b/go/adk/pkg/a2a/agentcard.go index 30dc86dd52..cd5d79ef3c 100644 --- a/go/adk/pkg/a2a/agentcard.go +++ b/go/adk/pkg/a2a/agentcard.go @@ -2,8 +2,8 @@ package a2a import ( a2atype "github.com/a2aproject/a2a-go/a2a" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. ) // EnrichAgentCard populates the agent card with skills derived from the ADK diff --git a/go/adk/pkg/a2a/consts.go b/go/adk/pkg/a2a/consts.go index f294a68910..0526b09cf4 100644 --- a/go/adk/pkg/a2a/consts.go +++ b/go/adk/pkg/a2a/consts.go @@ -1,6 +1,6 @@ package a2a -import "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. +import "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. const ( StateKeySessionName = "session_name" diff --git a/go/adk/pkg/a2a/converter.go b/go/adk/pkg/a2a/converter.go index e1fef222f1..ca45a7094c 100644 --- a/go/adk/pkg/a2a/converter.go +++ b/go/adk/pkg/a2a/converter.go @@ -6,8 +6,8 @@ import ( "maps" a2atype "github.com/a2aproject/a2a-go/a2a" - "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. - adksession "google.golang.org/adk/session" + "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. + adksession "google.golang.org/adk/v2/session" "google.golang.org/genai" ) diff --git a/go/adk/pkg/a2a/converter_test.go b/go/adk/pkg/a2a/converter_test.go index 806e394820..c9ec15a908 100644 --- a/go/adk/pkg/a2a/converter_test.go +++ b/go/adk/pkg/a2a/converter_test.go @@ -5,7 +5,7 @@ import ( "testing" a2atype "github.com/a2aproject/a2a-go/a2a" - "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. + "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. "google.golang.org/genai" ) diff --git a/go/adk/pkg/a2a/executor.go b/go/adk/pkg/a2a/executor.go index e92fc7df1c..0c8ea8dc38 100644 --- a/go/adk/pkg/a2a/executor.go +++ b/go/adk/pkg/a2a/executor.go @@ -17,9 +17,9 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/skills" "github.com/kagent-dev/kagent/go/adk/pkg/telemetry" "go.opentelemetry.io/otel/attribute" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/runner" - "google.golang.org/adk/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/runner" + "google.golang.org/adk/v2/server/adka2a" //nolint:staticcheck // kagent still uses a2a-go v1; this ADK package is the compatibility adapter. ) const ( diff --git a/go/adk/pkg/a2a/hitl.go b/go/adk/pkg/a2a/hitl.go index e8e716b72e..3d60eb8808 100644 --- a/go/adk/pkg/a2a/hitl.go +++ b/go/adk/pkg/a2a/hitl.go @@ -5,7 +5,7 @@ import ( "maps" a2atype "github.com/a2aproject/a2a-go/a2a" - "google.golang.org/adk/tool/toolconfirmation" + "google.golang.org/adk/v2/tool/toolconfirmation" ) const ( diff --git a/go/adk/pkg/agent/agent.go b/go/adk/pkg/agent/agent.go index fa9d633d14..621739862d 100644 --- a/go/adk/pkg/agent/agent.go +++ b/go/adk/pkg/agent/agent.go @@ -13,13 +13,13 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/sts" "github.com/kagent-dev/kagent/go/adk/pkg/tools" "github.com/kagent-dev/kagent/go/api/adk" - "google.golang.org/adk/agent" - "google.golang.org/adk/agent/llmagent" - adkmodel "google.golang.org/adk/model" - adkgemini "google.golang.org/adk/model/gemini" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/loadmemorytool" - "google.golang.org/adk/tool/preloadmemorytool" + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + adkmodel "google.golang.org/adk/v2/model" + adkgemini "google.golang.org/adk/v2/model/gemini" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/loadmemorytool" + "google.golang.org/adk/v2/tool/preloadmemorytool" "google.golang.org/genai" ) @@ -367,7 +367,7 @@ func extractHeaders(headers map[string]string) map[string]string { // makeBeforeToolCallback returns a BeforeToolCallback that logs tool invocations. func makeBeforeToolCallback(logger logr.Logger) llmagent.BeforeToolCallback { - return func(ctx agent.ToolContext, t tool.Tool, args map[string]any) (map[string]any, error) { + return func(ctx agent.Context, t tool.Tool, args map[string]any) (map[string]any, error) { logger.Info("Tool execution started", "tool", t.Name(), "functionCallID", ctx.FunctionCallID(), @@ -381,7 +381,7 @@ func makeBeforeToolCallback(logger logr.Logger) llmagent.BeforeToolCallback { // makeAfterToolCallback returns an AfterToolCallback that logs tool completion. func makeAfterToolCallback(logger logr.Logger) llmagent.AfterToolCallback { - return func(ctx agent.ToolContext, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) { + return func(ctx agent.Context, t tool.Tool, args, result map[string]any, err error) (map[string]any, error) { if err != nil { logger.Error(err, "Tool execution completed with error", "tool", t.Name(), @@ -404,7 +404,7 @@ func makeAfterToolCallback(logger logr.Logger) llmagent.AfterToolCallback { // makeOnToolErrorCallback returns an OnToolErrorCallback that logs tool errors. func makeOnToolErrorCallback(logger logr.Logger) llmagent.OnToolErrorCallback { - return func(ctx agent.ToolContext, t tool.Tool, args map[string]any, err error) (map[string]any, error) { + return func(ctx agent.Context, t tool.Tool, args map[string]any, err error) (map[string]any, error) { logger.Error(err, "Tool execution failed", "tool", t.Name(), "functionCallID", ctx.FunctionCallID(), diff --git a/go/adk/pkg/agent/approval.go b/go/adk/pkg/agent/approval.go index 535d6d6690..f40c31e9ea 100644 --- a/go/adk/pkg/agent/approval.go +++ b/go/adk/pkg/agent/approval.go @@ -3,10 +3,10 @@ package agent import ( "fmt" - "google.golang.org/adk/agent" - "google.golang.org/adk/agent/llmagent" - adkmodel "google.golang.org/adk/model" - "google.golang.org/adk/tool" + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + adkmodel "google.golang.org/adk/v2/model" + "google.golang.org/adk/v2/tool" "google.golang.org/genai" ) @@ -16,7 +16,7 @@ import ( // HITL events the LLM never produced and does not need to reason about. // The session still stores them so ADK's resume machinery can find them. func MakeStripConfirmationPartsCallback() llmagent.BeforeModelCallback { - return func(_ agent.CallbackContext, req *adkmodel.LLMRequest) (*adkmodel.LLMResponse, error) { + return func(_ agent.Context, req *adkmodel.LLMRequest) (*adkmodel.LLMResponse, error) { out := make([]*genai.Content, 0, len(req.Contents)) for _, c := range req.Contents { if c == nil { @@ -53,7 +53,7 @@ func MakeStripConfirmationPartsCallback() llmagent.BeforeModelCallback { // tools in the approval set behind request_confirmation / ToolConfirmation. // Port of kagent-adk/src/kagent/adk/_approval.py:make_approval_callback(). func MakeApprovalCallback(toolsRequiringApproval map[string]bool) llmagent.BeforeToolCallback { - return func(ctx agent.ToolContext, t tool.Tool, args map[string]any) (map[string]any, error) { + return func(ctx agent.Context, t tool.Tool, args map[string]any) (map[string]any, error) { toolName := t.Name() // No approval needed for this tool. diff --git a/go/adk/pkg/agent/createllm_test.go b/go/adk/pkg/agent/createllm_test.go index 9330aa1c71..1722b2a9fe 100644 --- a/go/adk/pkg/agent/createllm_test.go +++ b/go/adk/pkg/agent/createllm_test.go @@ -12,9 +12,9 @@ import ( "github.com/kagent-dev/mockllm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/runner" - adksession "google.golang.org/adk/session" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/runner" + adksession "google.golang.org/adk/v2/session" "google.golang.org/genai" ) diff --git a/go/adk/pkg/app/app.go b/go/adk/pkg/app/app.go index 36d5fec69f..78da730065 100644 --- a/go/adk/pkg/app/app.go +++ b/go/adk/pkg/app/app.go @@ -19,7 +19,7 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/taskstore" "go.uber.org/zap" "go.uber.org/zap/zapcore" - adkagent "google.golang.org/adk/agent" + adkagent "google.golang.org/adk/v2/agent" ) const ( diff --git a/go/adk/pkg/mcp/registry.go b/go/adk/pkg/mcp/registry.go index 97cf20ea41..8671e4416f 100644 --- a/go/adk/pkg/mcp/registry.go +++ b/go/adk/pkg/mcp/registry.go @@ -14,8 +14,8 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/constants" "github.com/kagent-dev/kagent/go/api/adk" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/mcptoolset" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/mcptoolset" ) // DynamicHeaderProvider is a function that returns headers to inject into MCP requests. diff --git a/go/adk/pkg/memory/kagent_service.go b/go/adk/pkg/memory/kagent_service.go index d58171a64a..c50d00226a 100644 --- a/go/adk/pkg/memory/kagent_service.go +++ b/go/adk/pkg/memory/kagent_service.go @@ -12,9 +12,9 @@ import ( "github.com/go-logr/logr" "github.com/kagent-dev/kagent/go/adk/pkg/embedding" "github.com/kagent-dev/kagent/go/api/adk" - "google.golang.org/adk/memory" - adkmodel "google.golang.org/adk/model" - adksession "google.golang.org/adk/session" + "google.golang.org/adk/v2/memory" + adkmodel "google.golang.org/adk/v2/model" + adksession "google.golang.org/adk/v2/session" "google.golang.org/genai" ) diff --git a/go/adk/pkg/memory/kagent_service_test.go b/go/adk/pkg/memory/kagent_service_test.go index eaac249b87..07bdff6892 100644 --- a/go/adk/pkg/memory/kagent_service_test.go +++ b/go/adk/pkg/memory/kagent_service_test.go @@ -12,8 +12,8 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/embedding" "github.com/kagent-dev/kagent/go/api/adk" - "google.golang.org/adk/memory" - adksession "google.golang.org/adk/session" + "google.golang.org/adk/v2/memory" + adksession "google.golang.org/adk/v2/session" "google.golang.org/genai" ) diff --git a/go/adk/pkg/memory/save_memory_tool.go b/go/adk/pkg/memory/save_memory_tool.go index 9459aa5c4c..b0fa27aceb 100644 --- a/go/adk/pkg/memory/save_memory_tool.go +++ b/go/adk/pkg/memory/save_memory_tool.go @@ -3,9 +3,9 @@ package memory import ( "fmt" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/functiontool" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" ) type saveMemoryInput struct { @@ -17,7 +17,7 @@ func NewSaveMemoryTool(svc *KagentMemoryService) (tool.Tool, error) { return functiontool.New(functiontool.Config{ Name: "save_memory", Description: "Saves a specific piece of information or text to long-term memory. Use this to remember important facts, user preferences, or specific details for future reference.", - }, func(toolCtx adkagent.ToolContext, in saveMemoryInput) (map[string]any, error) { + }, func(toolCtx adkagent.Context, in saveMemoryInput) (map[string]any, error) { if in.Content == "" { return nil, fmt.Errorf("missing required parameter: content") } diff --git a/go/adk/pkg/models/anthropic_adk.go b/go/adk/pkg/models/anthropic_adk.go index 77d8f2265e..c51a7da98c 100644 --- a/go/adk/pkg/models/anthropic_adk.go +++ b/go/adk/pkg/models/anthropic_adk.go @@ -11,7 +11,7 @@ import ( "github.com/anthropics/anthropic-sdk-go" "github.com/kagent-dev/kagent/go/adk/pkg/telemetry" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/models/bedrock.go b/go/adk/pkg/models/bedrock.go index f6049c3d26..950a5778c6 100644 --- a/go/adk/pkg/models/bedrock.go +++ b/go/adk/pkg/models/bedrock.go @@ -15,7 +15,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/go-logr/logr" "github.com/kagent-dev/kagent/go/adk/pkg/telemetry" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/models/ollama_adk.go b/go/adk/pkg/models/ollama_adk.go index c9571b9214..64d0cf6614 100644 --- a/go/adk/pkg/models/ollama_adk.go +++ b/go/adk/pkg/models/ollama_adk.go @@ -9,7 +9,7 @@ import ( "github.com/google/uuid" "github.com/kagent-dev/kagent/go/adk/pkg/telemetry" "github.com/ollama/ollama/api" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/models/openai_adk.go b/go/adk/pkg/models/openai_adk.go index dd5b3e6d21..41aef395c2 100644 --- a/go/adk/pkg/models/openai_adk.go +++ b/go/adk/pkg/models/openai_adk.go @@ -17,7 +17,7 @@ import ( "github.com/openai/openai-go/v3/packages/respjson" "github.com/openai/openai-go/v3/shared" "github.com/openai/openai-go/v3/shared/constant" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/models/sapaicore_adk.go b/go/adk/pkg/models/sapaicore_adk.go index 543f4b1e6c..ae4598b426 100644 --- a/go/adk/pkg/models/sapaicore_adk.go +++ b/go/adk/pkg/models/sapaicore_adk.go @@ -13,7 +13,7 @@ import ( "slices" "strings" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/models/sapaicore_adk_test.go b/go/adk/pkg/models/sapaicore_adk_test.go index 93037da8ba..a37ba74405 100644 --- a/go/adk/pkg/models/sapaicore_adk_test.go +++ b/go/adk/pkg/models/sapaicore_adk_test.go @@ -13,7 +13,7 @@ import ( "time" "github.com/go-logr/logr" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/runner/adapter.go b/go/adk/pkg/runner/adapter.go index b769d3fe04..dfa607dfe2 100644 --- a/go/adk/pkg/runner/adapter.go +++ b/go/adk/pkg/runner/adapter.go @@ -14,11 +14,11 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/sts" "github.com/kagent-dev/kagent/go/adk/pkg/tools" "github.com/kagent-dev/kagent/go/api/adk" - adkmemory "google.golang.org/adk/memory" - adkplugin "google.golang.org/adk/plugin" - "google.golang.org/adk/runner" - adksession "google.golang.org/adk/session" - adktool "google.golang.org/adk/tool" + adkmemory "google.golang.org/adk/v2/memory" + adkplugin "google.golang.org/adk/v2/plugin" + "google.golang.org/adk/v2/runner" + adksession "google.golang.org/adk/v2/session" + adktool "google.golang.org/adk/v2/tool" ) func agentNameFromAppName(appName string) string { diff --git a/go/adk/pkg/session/local_session.go b/go/adk/pkg/session/local_session.go index a2f43d190c..bfb55a8edc 100644 --- a/go/adk/pkg/session/local_session.go +++ b/go/adk/pkg/session/local_session.go @@ -8,7 +8,7 @@ import ( "sync" "time" - adksession "google.golang.org/adk/session" + adksession "google.golang.org/adk/v2/session" ) // localSession implements adksession.Session with mutex-guarded state. diff --git a/go/adk/pkg/session/session.go b/go/adk/pkg/session/session.go index a8a3cd6dc3..8daa198bbc 100644 --- a/go/adk/pkg/session/session.go +++ b/go/adk/pkg/session/session.go @@ -13,7 +13,7 @@ import ( "github.com/go-logr/logr" "github.com/google/uuid" - adksession "google.golang.org/adk/session" + adksession "google.golang.org/adk/v2/session" ) const ( diff --git a/go/adk/pkg/session/session_test.go b/go/adk/pkg/session/session_test.go index 4d3130b8fb..8e9537ab2f 100644 --- a/go/adk/pkg/session/session_test.go +++ b/go/adk/pkg/session/session_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - adksession "google.golang.org/adk/session" + adksession "google.golang.org/adk/v2/session" ) func mustJSON(t *testing.T, v any) []byte { diff --git a/go/adk/pkg/sts/plugin.go b/go/adk/pkg/sts/plugin.go index 39fe26f5fe..3d76a6b286 100644 --- a/go/adk/pkg/sts/plugin.go +++ b/go/adk/pkg/sts/plugin.go @@ -9,8 +9,8 @@ import ( "github.com/go-logr/logr" "github.com/golang-jwt/jwt/v5" "github.com/kagent-dev/kagent/go/adk/pkg/models" - "google.golang.org/adk/agent" - adkplugin "google.golang.org/adk/plugin" + "google.golang.org/adk/v2/agent" + adkplugin "google.golang.org/adk/v2/plugin" "google.golang.org/genai" ) diff --git a/go/adk/pkg/sts/plugin_test.go b/go/adk/pkg/sts/plugin_test.go index 8bf15cb0bf..9ee2c652f0 100644 --- a/go/adk/pkg/sts/plugin_test.go +++ b/go/adk/pkg/sts/plugin_test.go @@ -11,8 +11,8 @@ import ( "github.com/go-logr/logr" "github.com/golang-jwt/jwt/v5" kagentmodels "github.com/kagent-dev/kagent/go/adk/pkg/models" - "google.golang.org/adk/agent" - "google.golang.org/adk/session" + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/session" "google.golang.org/genai" ) @@ -37,14 +37,19 @@ func (f fakeInvocationContext) Memory() agent.Memory { return nil } func (f fakeInvocationContext) Session() session.Session { return fakeSession{id: f.sessionID} } func (f fakeInvocationContext) InvocationID() string { return "" } func (f fakeInvocationContext) Branch() string { return "" } +func (f fakeInvocationContext) IsolationScope() string { return "" } func (f fakeInvocationContext) UserContent() *genai.Content { return nil } func (f fakeInvocationContext) RunConfig() *agent.RunConfig { return nil } func (f *fakeInvocationContext) EndInvocation() { f.ended = true } func (f fakeInvocationContext) Ended() bool { return f.ended } +func (f fakeInvocationContext) ResumedInput(string) (any, bool) { return nil, false } func (f fakeInvocationContext) WithContext(ctx context.Context) agent.InvocationContext { f.Context = ctx return &f } +func (f fakeInvocationContext) WithICDelta(*agent.InvocationContextDelta) agent.InvocationContext { + return &f +} type fakeSession struct { id string diff --git a/go/adk/pkg/telemetry/attributes.go b/go/adk/pkg/telemetry/attributes.go index 59227d4e7f..ecfa64de74 100644 --- a/go/adk/pkg/telemetry/attributes.go +++ b/go/adk/pkg/telemetry/attributes.go @@ -10,7 +10,7 @@ import ( "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" ) const ( diff --git a/go/adk/pkg/telemetry/attributes_test.go b/go/adk/pkg/telemetry/attributes_test.go index a58cac7003..4e689d68ec 100644 --- a/go/adk/pkg/telemetry/attributes_test.go +++ b/go/adk/pkg/telemetry/attributes_test.go @@ -9,7 +9,7 @@ import ( "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" - "google.golang.org/adk/model" + "google.golang.org/adk/v2/model" "google.golang.org/genai" ) diff --git a/go/adk/pkg/telemetry/tracing.go b/go/adk/pkg/telemetry/tracing.go index 695ec0711e..9c01f9fcfc 100644 --- a/go/adk/pkg/telemetry/tracing.go +++ b/go/adk/pkg/telemetry/tracing.go @@ -19,7 +19,7 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/trace" - adktelemetry "google.golang.org/adk/telemetry" + adktelemetry "google.golang.org/adk/v2/telemetry" ) // SetKAgentSpanAttributes sets kagent span attributes in the OpenTelemetry context diff --git a/go/adk/pkg/tools/ask_user.go b/go/adk/pkg/tools/ask_user.go index 9568d3d776..9e9258bb69 100644 --- a/go/adk/pkg/tools/ask_user.go +++ b/go/adk/pkg/tools/ask_user.go @@ -5,9 +5,9 @@ import ( "fmt" "strings" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/functiontool" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" ) type askUserQuestion struct { @@ -40,7 +40,7 @@ func NewAskUserTool() (tool.Tool, error) { return functiontool.New(functiontool.Config{ Name: "ask_user", Description: askUserDescription, - }, func(ctx adkagent.ToolContext, in askUserInput) (map[string]any, error) { + }, func(ctx adkagent.Context, in askUserInput) (map[string]any, error) { if ctx.ToolConfirmation() == nil { // Phase 1 — pause execution and ask the user. var sb strings.Builder diff --git a/go/adk/pkg/tools/remote_a2a_tool.go b/go/adk/pkg/tools/remote_a2a_tool.go index c56b787dbd..705c5c9a68 100644 --- a/go/adk/pkg/tools/remote_a2a_tool.go +++ b/go/adk/pkg/tools/remote_a2a_tool.go @@ -15,9 +15,9 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/a2a" "github.com/kagent-dev/kagent/go/adk/pkg/constants" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/functiontool" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" ) // userIDContextKey is the context key for passing the session user_id to the subagent. @@ -177,7 +177,7 @@ func NewKAgentRemoteA2ATool(name, description, baseURL string, httpClient *http. ft, err := functiontool.New(functiontool.Config{ Name: name, Description: description, - }, func(ctx adkagent.ToolContext, in remoteA2AInput) (map[string]any, error) { + }, func(ctx adkagent.Context, in remoteA2AInput) (map[string]any, error) { return state.run(ctx, in.Request) }) if err != nil { @@ -239,7 +239,7 @@ func (s *remoteA2AState) ensureClient(ctx context.Context) (*a2aclient.Client, e } // run dispatches to handleResume or handleFirstCall based on ToolConfirmation presence. -func (s *remoteA2AState) run(ctx adkagent.ToolContext, requestText string) (map[string]any, error) { +func (s *remoteA2AState) run(ctx adkagent.Context, requestText string) (map[string]any, error) { if ctx.ToolConfirmation() != nil { return s.handleResume(ctx) } @@ -247,7 +247,7 @@ func (s *remoteA2AState) run(ctx adkagent.ToolContext, requestText string) (map[ } // handleFirstCall is Phase 1: send the request to the remote agent. -func (s *remoteA2AState) handleFirstCall(ctx adkagent.ToolContext, requestText string) (map[string]any, error) { +func (s *remoteA2AState) handleFirstCall(ctx adkagent.Context, requestText string) (map[string]any, error) { if requestText == "" { return map[string]any{"error": "missing or empty 'request' argument"}, nil } @@ -275,7 +275,7 @@ func (s *remoteA2AState) handleFirstCall(ctx adkagent.ToolContext, requestText s } // handleResume is Phase 2: forward the user's decision to the remote agent's pending task. -func (s *remoteA2AState) handleResume(ctx adkagent.ToolContext) (map[string]any, error) { +func (s *remoteA2AState) handleResume(ctx adkagent.Context) (map[string]any, error) { confirmation := ctx.ToolConfirmation() payload, _ := confirmation.Payload.(map[string]any) hitlPayload := a2a.ParseHitlConfirmationPayload(payload) @@ -337,7 +337,7 @@ func (s *remoteA2AState) handleResume(ctx adkagent.ToolContext) (map[string]any, } // processResult converts a SendMessageResult into a tool return value. -func (s *remoteA2AState) processResult(ctx adkagent.ToolContext, result a2atype.SendMessageResult) (map[string]any, error) { +func (s *remoteA2AState) processResult(ctx adkagent.Context, result a2atype.SendMessageResult) (map[string]any, error) { switch r := result.(type) { case *a2atype.Message: return map[string]any{"result": extractTextFromMessage(r)}, nil @@ -371,7 +371,7 @@ func (s *remoteA2AState) processResult(ctx adkagent.ToolContext, result a2atype. } // handleInputRequired pauses parent agent execution via RequestConfirmation. -func (s *remoteA2AState) handleInputRequired(ctx adkagent.ToolContext, task *a2atype.Task) map[string]any { +func (s *remoteA2AState) handleInputRequired(ctx adkagent.Context, task *a2atype.Task) map[string]any { if task == nil { slog.Error("Subagent returned input_required without task", "tool", s.name) return map[string]any{ diff --git a/go/adk/pkg/tools/share_tools.go b/go/adk/pkg/tools/share_tools.go index bff5d111e0..deb193fdd7 100644 --- a/go/adk/pkg/tools/share_tools.go +++ b/go/adk/pkg/tools/share_tools.go @@ -10,9 +10,9 @@ import ( "os" "strings" - "google.golang.org/adk/agent" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/functiontool" + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" ) // shareClient holds the dependencies for share link tools, captured at construction time. @@ -97,7 +97,7 @@ func NewCreateShareLinkTool(httpClient *http.Client, baseURL, appName string) (t "The link is read-only by default (visitors cannot send messages). " + "Set read_only=false to allow visitors to interact. " + "Each call creates a new token; existing tokens remain valid.", - }, func(ctx agent.ToolContext, in createShareInput) (map[string]any, error) { + }, func(ctx agent.Context, in createShareInput) (map[string]any, error) { sessionID := ctx.SessionID() if sessionID == "" { return nil, fmt.Errorf("create_share_link: no session ID in context") @@ -142,7 +142,7 @@ func NewListShareLinksTool(httpClient *http.Client, baseURL, appName string) (to Description: "Lists all active share links for the current session. " + "Returns each share token and creation time. " + "Use this to find a token before calling delete_share_link.", - }, func(ctx agent.ToolContext, _ struct{}) (map[string]any, error) { + }, func(ctx agent.Context, _ struct{}) (map[string]any, error) { sessionID := ctx.SessionID() if sessionID == "" { return nil, fmt.Errorf("list_share_links: no session ID in context") @@ -184,7 +184,7 @@ func NewDeleteShareLinkTool(httpClient *http.Client, baseURL, appName string) (t Name: "delete_share_link", Description: "Deletes a share link by token, immediately revoking access for anyone using it. " + "Use list_share_links first to find the token you want to revoke.", - }, func(ctx agent.ToolContext, in deleteShareInput) (map[string]any, error) { + }, func(ctx agent.Context, in deleteShareInput) (map[string]any, error) { if in.Token == "" { return nil, fmt.Errorf("delete_share_link: token is required") } diff --git a/go/adk/pkg/tools/skills.go b/go/adk/pkg/tools/skills.go index f4253fac49..d549781e28 100644 --- a/go/adk/pkg/tools/skills.go +++ b/go/adk/pkg/tools/skills.go @@ -7,9 +7,9 @@ import ( "strings" skillruntime "github.com/kagent-dev/kagent/go/adk/pkg/skills" - adkagent "google.golang.org/adk/agent" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/functiontool" + adkagent "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" ) const ( @@ -122,7 +122,7 @@ func NewSkillsTools(skillsDirectory string) ([]tool.Tool, error) { skillsTool, err := functiontool.New(functiontool.Config{ Name: "skills", Description: skillruntime.GenerateSkillsToolDescription(discoveredSkills), - }, func(ctx adkagent.ToolContext, in skillsInput) (string, error) { + }, func(ctx adkagent.Context, in skillsInput) (string, error) { skillName := strings.TrimSpace(in.Command) if skillName == "" { return "Error: No skill name provided", nil @@ -147,7 +147,7 @@ func NewSkillsTools(skillsDirectory string) ([]tool.Tool, error) { readFileTool, err := functiontool.New(functiontool.Config{ Name: "read_file", Description: readFileDescription, - }, func(ctx adkagent.ToolContext, in readFileInput) (string, error) { + }, func(ctx adkagent.Context, in readFileInput) (string, error) { path, err := resolveReadPath(ctx.SessionID(), absSkillsDir, in.FilePath) if err != nil { return fmt.Sprintf("Error reading file %s: %v", strings.TrimSpace(in.FilePath), err), nil @@ -166,7 +166,7 @@ func NewSkillsTools(skillsDirectory string) ([]tool.Tool, error) { writeFileTool, err := functiontool.New(functiontool.Config{ Name: "write_file", Description: writeFileDescription, - }, func(ctx adkagent.ToolContext, in writeFileInput) (string, error) { + }, func(ctx adkagent.Context, in writeFileInput) (string, error) { path, err := resolveWritePath(ctx.SessionID(), absSkillsDir, in.FilePath) if err != nil { return fmt.Sprintf("Error writing file %s: %v", strings.TrimSpace(in.FilePath), err), nil @@ -184,7 +184,7 @@ func NewSkillsTools(skillsDirectory string) ([]tool.Tool, error) { editFileTool, err := functiontool.New(functiontool.Config{ Name: "edit_file", Description: editFileDescription, - }, func(ctx adkagent.ToolContext, in editFileInput) (string, error) { + }, func(ctx adkagent.Context, in editFileInput) (string, error) { path, err := resolveEditPath(ctx.SessionID(), absSkillsDir, in.FilePath) if err != nil { return fmt.Sprintf("Error editing file %s: %v", strings.TrimSpace(in.FilePath), err), nil @@ -202,7 +202,7 @@ func NewSkillsTools(skillsDirectory string) ([]tool.Tool, error) { bashTool, err := functiontool.New(functiontool.Config{ Name: "bash", Description: bashDescription, - }, func(ctx adkagent.ToolContext, in bashInput) (string, error) { + }, func(ctx adkagent.Context, in bashInput) (string, error) { command := strings.TrimSpace(in.Command) if command == "" { return "Error: No command provided", nil diff --git a/go/go.mod b/go/go.mod index 10b447672b..4915a32f30 100644 --- a/go/go.mod +++ b/go/go.mod @@ -47,7 +47,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 golang.org/x/text v0.38.0 - google.golang.org/adk v1.4.0 + google.golang.org/adk/v2 v2.0.0 google.golang.org/genai v1.62.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 diff --git a/go/go.sum b/go/go.sum index b4cca2391e..ca62087c3f 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1021,8 +1021,8 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/adk v1.4.0 h1:Qi4KB9YKD00/I5K9v3QsZ9ng5YiZQ7MfMgM8BZjNcsM= -google.golang.org/adk v1.4.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4= +google.golang.org/adk/v2 v2.0.0 h1:7eRbsnv0XkQPVctf8qtQ+KuO8XjkBrMNxznY6OA/sTs= +google.golang.org/adk/v2 v2.0.0/go.mod h1:fPuMPT5s3LsWu97mdeFjTPZu/02tIALWRWeqHL2FWKE= google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74= google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/genai v1.62.0 h1:PaBju84orf4Vbcc6OfHe4vxhxhjwulKTgOpEc3iIc00= From c99cb90d7bbfae365380bc1de983ddd0816dbea0 Mon Sep 17 00:00:00 2001 From: Yaniv Marom Nachumi Date: Sun, 5 Jul 2026 14:39:08 +0300 Subject: [PATCH 2/2] docs: add EP for ADK workflow agent types (Sequential/Parallel/Loop) Add a provisional enhancement proposal to expose the Google ADK workflow agent types through the declarative Agent CRD. Today every declarative agent compiles to a single LlmAgent; this EP proposes a `kind` discriminator plus sub-agent references so users can build deterministic Sequential, Parallel, and Loop workflows natively. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Yaniv Marom Nachumi --- design/EP-XXXX-adk-agent-types.md | 287 ++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 design/EP-XXXX-adk-agent-types.md diff --git a/design/EP-XXXX-adk-agent-types.md b/design/EP-XXXX-adk-agent-types.md new file mode 100644 index 0000000000..f5c1e11325 --- /dev/null +++ b/design/EP-XXXX-adk-agent-types.md @@ -0,0 +1,287 @@ +# EP-XXXX: Support for ADK workflow agent types (Sequential, Parallel, Loop) + +* Issue: [#XXXX](https://github.com/kagent-dev/kagent/issues/XXXX) +* Status: `provisional` + +## Background + +A kagent declarative agent is described by `DeclarativeAgentSpec` and compiled by +the Go runtime into a single Google ADK agent. Today that compilation is +hard-coded: [`CreateGoogleADKAgentWithSubagentSessionIDs`](../go/adk/pkg/agent/agent.go) +always calls `llmagent.New(...)`, so **every** declarative agent is an +`LlmAgent` — a single model reasoning over a tool set. + +The Google ADK, however, ships several first-class agent types. As of the v2.0.0 +upgrade the following are available under `google.golang.org/adk/v2/agent`: + +| ADK type | Package | Behavior | +|----------|---------|----------| +| `LlmAgent` | `agent/llmagent` | A model reasons over tools/instructions. **The only type kagent uses today.** | +| `SequentialAgent` | `agent/workflowagents/sequentialagent` | Runs sub-agents in order; state flows forward between them. | +| `ParallelAgent` | `agent/workflowagents/parallelagent` | Runs sub-agents concurrently in isolation; useful for multi-perspective fan-out. | +| `LoopAgent` | `agent/workflowagents/loopagent` | Runs sub-agents repeatedly until `MaxIterations` or a sub-agent escalates. | + +The three workflow types (`Sequential`, `Parallel`, `Loop`) are *deterministic +orchestrators*. They do not call an LLM to decide control flow — they run their +`SubAgents()` according to a fixed policy and pass data between them through +session state (each `LlmAgent` can publish its result under an `OutputKey`, which +downstream agents read from state). + +kagent already has **one** composition mechanism: an agent can reference other +agents as tools (`ToolProviderType_Agent` / `spec.declarative.tools[].agent` and +the `RemoteAgents` A2A wiring). That is *LLM-driven delegation* — the calling +model decides, at runtime and non-deterministically, whether and when to invoke a +sub-agent. It cannot express "always run A then B", "run A, B, and C at once and +merge", or "keep refining until the reviewer approves". + +This EP proposes exposing the ADK workflow agent types through the declarative +`Agent` CRD so users can build deterministic multi-agent workflows natively. + +## Motivation + +Multi-agent orchestration patterns are a core reason teams adopt an agent +framework, and the ADK already implements them robustly. Kagent leaves them +unreachable because the CRD → runtime path only knows how to emit an `LlmAgent`. + +Concrete use-cases that are impossible or unreliable today: + +- **Pipelines (Sequential):** "summarize → translate → fact-check", where each + stage must run, in order, exactly once. Agent-as-tool cannot guarantee the + model won't skip or reorder steps. +- **Fan-out / ensemble (Parallel):** run N specialist agents on the same input + and merge — e.g. three reviewers with different lenses, then a synthesizer. +- **Iterative refinement (Loop):** "generate → critique → revise" until a + reviewer agent signals completion or a bounded iteration cap is hit. + +These are exactly the patterns the ADK's `SequentialAgent`, `ParallelAgent`, and +`LoopAgent` exist to serve. The gap is purely in kagent's declarative surface and +its runtime factory, not in the underlying ADK. + +### Goals + +- Extend `DeclarativeAgentSpec` with an agent **kind** discriminator so an agent + can be declared as `LlmAgent` (default), `Sequential`, `Parallel`, or `Loop`. +- Allow a workflow agent to declare its ordered/unordered set of **sub-agents**, + referenced by name (other `Agent` resources in the same namespace). +- Compile the new kinds in the Go runtime via a factory that dispatches on kind + to the corresponding ADK constructor, attaching compiled sub-agents as + `SubAgents`. +- Support `maxIterations` for the `Loop` kind. +- Preserve full backward compatibility: any existing spec (no kind set) continues + to compile to an `LlmAgent` with identical behavior. + +### Non-Goals + +- **BYO agents.** Users who need arbitrary orchestration in code already have the + `BYO` agent type (`spec.byo`), which runs a user-provided container. This EP is + about the *declarative* path only. +- **Replacing agent-as-tool / A2A delegation.** LLM-driven delegation remains and + is complementary; workflow agents are for deterministic control flow. +- **Remote agents as a declarative kind.** ADK's `remoteagent` overlaps with the + existing `RemoteAgents` A2A wiring and is out of scope here. +- **A visual workflow builder in the UI.** The UI will need to render/edit the new + fields, but graph-editing UX is a separate effort. +- **Python runtime parity in the first cut.** Initial implementation targets the + Go runtime (`runtime: go`); see Open Questions. + +## Implementation Details + +### API changes (`v1alpha2`) + +Add a `kind` discriminator and a `subAgents` section to `DeclarativeAgentSpec`. +The model/instruction/tools fields remain and apply only when `kind: LlmAgent` +(the default). For workflow kinds those fields are unused and should be rejected +by a CEL validation rule. + +```go +// +kubebuilder:validation:Enum=LlmAgent;Sequential;Parallel;Loop +type DeclarativeAgentKind string + +const ( + DeclarativeAgentKind_Llm DeclarativeAgentKind = "LlmAgent" + DeclarativeAgentKind_Sequential DeclarativeAgentKind = "Sequential" + DeclarativeAgentKind_Parallel DeclarativeAgentKind = "Parallel" + DeclarativeAgentKind_Loop DeclarativeAgentKind = "Loop" +) + +type DeclarativeAgentSpec struct { + // Kind selects the ADK agent type. Defaults to LlmAgent, which preserves + // the single-model behavior of all existing agents. + // +optional + // +kubebuilder:default=LlmAgent + Kind DeclarativeAgentKind `json:"kind,omitempty"` + + // SubAgents lists the child agents composed by a workflow kind + // (Sequential/Parallel/Loop). Ignored for LlmAgent. + // Order is significant for Sequential and Loop. + // +optional + // +kubebuilder:validation:MaxItems=20 + SubAgents []SubAgentRef `json:"subAgents,omitempty"` + + // Workflow holds kind-specific settings (e.g. Loop iteration bounds). + // +optional + Workflow *WorkflowConfig `json:"workflow,omitempty"` + + // ... existing fields (SystemMessage, ModelConfig, Tools, ...) unchanged; + // they apply only when Kind == LlmAgent. +} + +// SubAgentRef references another Agent resource to compose as a child. +type SubAgentRef struct { + // Name of an Agent resource in the same namespace. + Name string `json:"name"` + // OutputKey, when set, publishes this sub-agent's final output into + // session state under this key so later sub-agents can read it. + // +optional + OutputKey string `json:"outputKey,omitempty"` +} + +type WorkflowConfig struct { + // MaxIterations bounds a Loop agent. 0 means run until a sub-agent + // escalates (maps to ADK loopagent MaxIterations). + // +optional + MaxIterations *uint `json:"maxIterations,omitempty"` +} +``` + +CEL validation to add on `DeclarativeAgentSpec`: + +- `kind != LlmAgent` ⟹ `subAgents` is non-empty and `modelConfig`/`tools` are unset. +- `kind == Loop` may set `workflow.maxIterations`; other kinds may not. +- `kind == LlmAgent` ⟹ `subAgents` is empty. + +Example — a sequential "research pipeline": + +```yaml +apiVersion: kagent.dev/v1alpha2 +kind: Agent +metadata: + name: research-pipeline + namespace: kagent +spec: + type: Declarative + declarative: + runtime: go + kind: Sequential + subAgents: + - name: researcher + outputKey: research_notes + - name: writer + outputKey: draft + - name: fact-checker +``` + +Example — a bounded refinement loop: + +```yaml +spec: + type: Declarative + declarative: + kind: Loop + workflow: + maxIterations: 5 + subAgents: + - name: drafter + outputKey: draft + - name: critic # escalates to end the loop when satisfied +``` + +### Runtime changes (Go ADK) + +Introduce a factory in [`go/adk/pkg/agent/agent.go`](../go/adk/pkg/agent/agent.go) +that dispatches on kind. Today the function body unconditionally builds an +`LlmAgent`; it becomes the `LlmAgent` branch of a switch: + +```go +switch cfg.Kind { +case adk.AgentKindSequential: + subs, err := buildSubAgents(ctx, cfg.SubAgents, log) + if err != nil { return nil, nil, err } + a, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: agentName, Description: cfg.Description, SubAgents: subs}, + }) + // ... +case adk.AgentKindParallel: + // parallelagent.New(parallelagent.Config{AgentConfig: agent.Config{... SubAgents: subs}}) +case adk.AgentKindLoop: + // loopagent.New(loopagent.Config{AgentConfig: agent.Config{... SubAgents: subs}, MaxIterations: cfg.MaxIterations}) +default: // LlmAgent — existing path, unchanged + llmAgent, err := llmagent.New(llmAgentConfig) + // ... +} +``` + +`buildSubAgents` compiles each referenced sub-agent into an `agent.Agent` and +applies its `OutputKey` (via `llmagent.Config.OutputKey`, which the ADK writes to +session state as a `StateDelta`). The workflow constructors then orchestrate +`ctx.Agent().SubAgents()` per their policy. + +Corresponding additions to the `adk.AgentConfig` transport type +([`go/api/adk`](../go/api/adk)) — a `Kind` field, a `SubAgents []SubAgentConfig` +list, and `MaxIterations` — plus the controller/translator work in +[`adk_api_translator.go`](../go/core/internal/controller/translator/agent/adk_api_translator.go) +to populate them from the CRD. + +### Sub-agent resolution + +Sub-agents are referenced **by name** (not inlined). Rationale, consistent with +the EP-685 tool-server decision: + +- Sub-agents are reusable across workflows and independently testable. +- Each sub-agent is a normal `Agent` resource, so RBAC, model config, and tool + wiring are unchanged and per-agent. +- The controller resolves references at compile time; a missing/failed reference + surfaces on the parent agent's status. Sub-agent references participate in the + reconcile watch set so parent agents recompile when a child changes. + +Inlining is considered and rejected in Alternatives. + +### Test Plan + +- **Unit (Go runtime):** table-driven tests over the factory — each kind builds + the expected ADK agent with the right `SubAgents`/`MaxIterations`; `OutputKey` + is propagated; unknown kind errors cleanly. Mock sub-agent compilation. +- **Unit (API):** CEL validation tests (mirroring `agent_spec_validation_test.go`) + for the kind/subAgents/model-field exclusivity and Loop-only `maxIterations`. +- **Translator:** CRD → `adk.AgentConfig` mapping for each kind, including + sub-agent reference resolution and error on dangling references. +- **E2E:** deploy a Sequential pipeline and a bounded Loop; assert ordered + execution / state hand-off via `OutputKey`, and that the Loop terminates at + `maxIterations`. Backward-compat E2E: an existing kind-less agent behaves + identically to before. + +## Alternatives + +**Inline sub-agent definitions.** Embed full sub-agent specs inside the parent +rather than referencing named `Agent` resources. Rejected: it duplicates model/ +tool configuration, prevents reuse and independent RBAC, and bloats a single CRD — +the same reasons EP-685 favored references over inlining for MCP servers. Named +references keep each agent a first-class, independently reconciled object. + +**Do nothing / lean on agent-as-tool.** Users can already expose agents as tools +and let a coordinator LLM delegate. Rejected as insufficient: delegation is +non-deterministic and cannot guarantee ordering, exhaustive fan-out, or bounded +iteration — the precise guarantees these workflow types provide. + +**Only add Sequential.** Sequential covers the most common pipeline case with the +least surface area. Rejected as the primary plan because Parallel and Loop reuse +the identical `SubAgents` machinery; adding them together avoids a second API +break, though a phased rollout (Sequential first) is a reasonable delivery order. + +## Open Questions + +- **Runtime parity.** Should the first cut require `runtime: go`, or must the + Python runtime reach parity before GA? The declarative surface is + runtime-agnostic; the Python compiler would need equivalent factory support. +- **State/output-key ergonomics.** Is per-sub-agent `outputKey` on the reference + the right place, or should it live on the sub-agent's own spec? Referencing it + from the parent keeps the child reusable but splits config across resources. +- **Nesting depth.** Workflow agents can contain workflow agents (a Sequential of + Parallels). Do we bound nesting depth, and how do we guard against reference + cycles at admission time? +- **Loop termination.** Beyond `maxIterations`, ADK loops end when a sub-agent + *escalates*. How do we let a declarative sub-agent signal escalation without + custom code — a convention on the reviewer agent, or a dedicated field? +- **A2A exposure.** Should a workflow agent be independently addressable over A2A + (`a2aConfig`) like an `LlmAgent`, and how are streaming events attributed to + sub-agents in the event author field?