Skip to content
Open
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
287 changes: 287 additions & 0 deletions design/EP-XXXX-adk-agent-types.md

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.

Is this design doc relevant to this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are two commits to this PR.

  1. Package version upgrade and adoption
  2. design to support additional agent types for ADK workflows

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.

Can we separate them? I'm happy to merge the bump, but would like time to look through the design

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure. will do

Original file line number Diff line number Diff line change
@@ -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`
Comment on lines +1 to +4

## 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?
12 changes: 6 additions & 6 deletions go/adk/examples/byo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 3 additions & 3 deletions go/adk/examples/oneshot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
4 changes: 2 additions & 2 deletions go/adk/pkg/a2a/agentcard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/a2a/consts.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions go/adk/pkg/a2a/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/a2a/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
6 changes: 3 additions & 3 deletions go/adk/pkg/a2a/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/a2a/hitl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading
Loading