Skip to content
Closed
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
98 changes: 84 additions & 14 deletions go/adk/pkg/tools/remote_a2a_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
"github.com/a2aproject/a2a-go/a2aclient/agentcard"
"github.com/kagent-dev/kagent/go/adk/pkg/a2a"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"google.golang.org/adk/model"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/genai"
)

// userIDContextKey is the context key for passing the session user_id to the subagent.
Expand All @@ -32,9 +33,87 @@ func (u *userIDForwardingInterceptor) Before(ctx context.Context, req *a2aclient
return ctx, nil
}

// remoteA2AInput is the typed argument for the remote A2A function tool.
type remoteA2AInput struct {
Request string `json:"request"`
// remoteA2ATool implements tool.Tool (and the ADK-internal FunctionTool and
// RequestProcessor interfaces) for delegating requests to a remote A2A agent.
//
// Unlike functiontool.New, this type populates Declaration().Parameters with a
// genai.Schema value rather than ParametersJsonSchema. Anthropic and AWS
// Bedrock model adapters inside the ADK read the Parameters field; they ignore
// ParametersJsonSchema, so using functiontool.New causes those adapters to omit
// the "request" parameter from the tool schema sent to the LLM. When the LLM
// receives no schema it guesses field names (e.g. "$task") and the call fails
// with "missing properties: [request]".
Comment on lines +40 to +45
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The comment claims Anthropic and Bedrock adapters "read the Parameters field" and "ignore ParametersJsonSchema", but in this repo Bedrock conversion checks ParametersJsonSchema first and Anthropic conversion currently only reads ParametersJsonSchema (go/adk/pkg/models/bedrock.go:512-522, go/adk/pkg/models/anthropic_adk.go:238-253). Please adjust the comment to match the actual compatibility matrix (or explicitly clarify it refers to upstream google.golang.org/adk adapters, not kagent's models).

Suggested change
// genai.Schema value rather than ParametersJsonSchema. Anthropic and AWS
// Bedrock model adapters inside the ADK read the Parameters field; they ignore
// ParametersJsonSchema, so using functiontool.New causes those adapters to omit
// the "request" parameter from the tool schema sent to the LLM. When the LLM
// receives no schema it guesses field names (e.g. "$task") and the call fails
// with "missing properties: [request]".
// genai.Schema value rather than relying only on ParametersJsonSchema. ADK
// adapters and conversion paths do not all consume tool schemas the same way,
// and ensuring Parameters is populated avoids cases where the "request"
// parameter is omitted from the tool schema sent to the LLM. When the LLM
// receives no usable schema it may guess field names (e.g. "$task"), and the
// call then fails with "missing properties: [request]".

Copilot uses AI. Check for mistakes.
type remoteA2ATool struct {
state *remoteA2AState
}

// Name implements tool.Tool.
func (t *remoteA2ATool) Name() string { return t.state.name }

// Description implements tool.Tool.
func (t *remoteA2ATool) Description() string { return t.state.description }

// IsLongRunning implements tool.Tool.
func (t *remoteA2ATool) IsLongRunning() bool { return false }

// Declaration returns an explicit genai.Schema so Anthropic/Bedrock adapters
// include "request" in the function schema they send to the LLM.
func (t *remoteA2ATool) Declaration() *genai.FunctionDeclaration {
return &genai.FunctionDeclaration{
Name: t.state.name,
Description: t.state.description,
Parameters: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"request": {
Type: genai.TypeString,
Comment on lines +61 to +69
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

remoteA2ATool.Declaration() sets only FunctionDeclaration.Parameters (genai.Schema). In this repo, the Anthropic model adapter currently builds tool schemas only from FunctionDeclaration.ParametersJsonSchema (see go/adk/pkg/models/anthropic_adk.go:238-253) and ignores Parameters. That means sub-agent calls will still be missing the required "request" field when using the Anthropic backend. To keep compatibility across adapters, either also populate ParametersJsonSchema here (e.g., as a small map[string]any JSON schema) or update the Anthropic adapter to handle Parameters as a fallback (like Bedrock/OpenAI do).

Copilot uses AI. Check for mistakes.
Description: "The request to send to the agent.",
},
},
Required: []string{"request"},
},
}
}

// Run implements the ADK-internal FunctionTool interface.
func (t *remoteA2ATool) Run(ctx tool.Context, args any) (map[string]any, error) {
margs, ok := args.(map[string]any)
if !ok {
return map[string]any{"error": "unexpected argument type"}, nil
}
request, _ := margs["request"].(string)
return t.state.run(ctx, request)
}

// ProcessRequest implements the ADK-internal RequestProcessor interface.
// It replicates the logic of toolutils.PackTool (which lives in an internal ADK
// package and cannot be imported from outside the module).
func (t *remoteA2ATool) ProcessRequest(_ tool.Context, req *model.LLMRequest) error {
if req.Tools == nil {
req.Tools = make(map[string]any)
}
if _, ok := req.Tools[t.Name()]; ok {
return fmt.Errorf("duplicate tool: %q", t.Name())
}
req.Tools[t.Name()] = t

if req.Config == nil {
req.Config = &genai.GenerateContentConfig{}
}
decl := t.Declaration()
if decl == nil {
return nil
}
for _, gt := range req.Config.Tools {
if gt != nil && gt.FunctionDeclarations != nil {
gt.FunctionDeclarations = append(gt.FunctionDeclarations, decl)
return nil
}
}
req.Config.Tools = append(req.Config.Tools, &genai.Tool{
FunctionDeclarations: []*genai.FunctionDeclaration{decl},
})
return nil
}

// remoteA2AState holds the mutable state for one remote A2A agent connection.
Expand Down Expand Up @@ -75,16 +154,7 @@ func NewKAgentRemoteA2ATool(name, description, baseURL string, httpClient *http.
extraHeaders: extraHeaders,
lastContextID: a2atype.NewContextID(),
}
ft, err := functiontool.New(functiontool.Config{
Name: name,
Description: description,
}, func(ctx tool.Context, in remoteA2AInput) (map[string]any, error) {
return state.run(ctx, in.Request)
})
if err != nil {
return nil, "", fmt.Errorf("failed to create remote A2A function tool for %s: %w", name, err)
}
return ft, state.lastContextID, nil
return &remoteA2ATool{state: state}, state.lastContextID, nil
}

// ensureClient lazily resolves the agent card and initialises the A2A client.
Expand Down
Loading