diff --git a/go/adk/pkg/tools/skills_test.go b/go/adk/pkg/tools/skills_test.go index 1e4758c9ef..f22a54aecd 100644 --- a/go/adk/pkg/tools/skills_test.go +++ b/go/adk/pkg/tools/skills_test.go @@ -21,8 +21,12 @@ func TestResolveReadPath_AllowsSymlinkedSkillsDirectory(t *testing.T) { if err != nil { t.Fatalf("resolveReadPath() error = %v", err) } - if resolved != skillFile { - t.Fatalf("resolveReadPath() = %q, want %q", resolved, skillFile) + want, err := filepath.EvalSymlinks(skillFile) + if err != nil { + t.Fatalf("EvalSymlinks() error = %v", err) + } + if resolved != want { + t.Fatalf("resolveReadPath() = %q, want %q", resolved, want) } } diff --git a/go/api/client/agent.go b/go/api/client/agent.go index d6bd0dd1ab..1a08c445ab 100644 --- a/go/api/client/agent.go +++ b/go/api/client/agent.go @@ -3,7 +3,6 @@ package client import ( "context" "fmt" - "net/url" api "github.com/kagent-dev/kagent/go/api/httpapi" "github.com/kagent-dev/kagent/go/api/v1alpha2" @@ -11,18 +10,13 @@ import ( // Agent defines the agent operations type Agent interface { - ListAgents(ctx context.Context, opts ...ListAgentsOptions) (*api.StandardResponse[[]api.AgentResponse], error) + ListAgents(ctx context.Context) (*api.StandardResponse[[]api.AgentResponse], error) CreateAgent(ctx context.Context, request *v1alpha2.Agent) (*api.StandardResponse[*v1alpha2.Agent], error) GetAgent(ctx context.Context, agentRef string) (*api.StandardResponse[*api.AgentResponse], error) UpdateAgent(ctx context.Context, request *v1alpha2.Agent) (*api.StandardResponse[*v1alpha2.Agent], error) DeleteAgent(ctx context.Context, agentRef string) error } -// ListAgentsOptions configures ListAgents requests. -type ListAgentsOptions struct { - Namespace string -} - // agentClient handles agent-related requests type agentClient struct { client *BaseClient @@ -33,23 +27,14 @@ func NewAgentClient(client *BaseClient) Agent { return &agentClient{client: client} } -// ListAgents lists all agents for a user. When Namespace is set, only agents in that namespace are returned. -func (c *agentClient) ListAgents(ctx context.Context, opts ...ListAgentsOptions) (*api.StandardResponse[[]api.AgentResponse], error) { - if len(opts) > 1 { - return nil, fmt.Errorf("ListAgents accepts at most one options argument") - } - +// ListAgents lists all agents for a user +func (c *agentClient) ListAgents(ctx context.Context) (*api.StandardResponse[[]api.AgentResponse], error) { userID := c.client.GetUserIDOrDefault("") if userID == "" { return nil, fmt.Errorf("userID is required") } - path := "/api/agents" - if len(opts) > 0 && opts[0].Namespace != "" { - path += "?namespace=" + url.QueryEscape(opts[0].Namespace) - } - - resp, err := c.client.Get(ctx, path, userID) + resp, err := c.client.Get(ctx, "/api/agents", userID) if err != nil { return nil, err } diff --git a/go/api/database/client.go b/go/api/database/client.go index 43943da678..312db52d95 100644 --- a/go/api/database/client.go +++ b/go/api/database/client.go @@ -4,9 +4,9 @@ import ( "context" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/pgvector/pgvector-go" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) type QueryOptions struct { @@ -24,8 +24,8 @@ type Client interface { StoreFeedback(ctx context.Context, feedback *Feedback) error StoreSession(ctx context.Context, session *Session) error StoreAgent(ctx context.Context, agent *Agent) error - StoreTask(ctx context.Context, task *protocol.Task) error - StorePushNotification(ctx context.Context, config *protocol.TaskPushNotificationConfig) error + StoreTask(ctx context.Context, task *a2a.Task) error + StorePushNotification(ctx context.Context, config *a2a.PushConfig) error StoreToolServer(ctx context.Context, toolServer *ToolServer) (*ToolServer, error) StoreEvents(ctx context.Context, messages ...*Event) error @@ -40,15 +40,15 @@ type Client interface { // Get methods GetSession(ctx context.Context, sessionID string, userID string) (*Session, error) GetAgent(ctx context.Context, name string) (*Agent, error) - GetTask(ctx context.Context, id string) (*protocol.Task, error) + GetTask(ctx context.Context, id string) (*a2a.Task, error) GetTool(ctx context.Context, name string) (*Tool, error) GetToolServer(ctx context.Context, name string) (*ToolServer, error) - GetPushNotification(ctx context.Context, taskID string, configID string) (*protocol.TaskPushNotificationConfig, error) + GetPushNotification(ctx context.Context, taskID string, configID string) (*a2a.PushConfig, error) // List methods ListTools(ctx context.Context) ([]Tool, error) ListFeedback(ctx context.Context, userID string) ([]Feedback, error) - ListTasksForSession(ctx context.Context, sessionID string) ([]*protocol.Task, error) + ListTasksForSession(ctx context.Context, sessionID string) ([]*a2a.Task, error) ListSessions(ctx context.Context, userID string) ([]Session, error) ListSessionsForAgent(ctx context.Context, agentID string, userID string) ([]Session, error) ListSessionsForAgentAllUsers(ctx context.Context, agentID string) ([]Session, error) @@ -56,7 +56,7 @@ type Client interface { ListToolServers(ctx context.Context) ([]ToolServer, error) ListToolsForServer(ctx context.Context, serverName string, groupKind string) ([]Tool, error) ListEventsForSession(ctx context.Context, sessionID, userID string, options QueryOptions) ([]*Event, error) - ListPushNotifications(ctx context.Context, taskID string) ([]*protocol.TaskPushNotificationConfig, error) + ListPushNotifications(ctx context.Context, taskID string) ([]*a2a.PushConfig, error) // Helper methods RefreshToolsForServer(ctx context.Context, serverName string, groupKind string, tools ...*v1alpha2.MCPTool) error diff --git a/go/api/database/models.go b/go/api/database/models.go index 7bb7be9daa..d0852fe922 100644 --- a/go/api/database/models.go +++ b/go/api/database/models.go @@ -4,10 +4,10 @@ import ( "encoding/json" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/adk" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/pgvector/pgvector-go" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) type Agent struct { @@ -32,16 +32,16 @@ type Event struct { Data string `json:"data"` // JSON-serialized protocol.Message } -func (m *Event) Parse() (protocol.Message, error) { - var data protocol.Message +func (m *Event) Parse() (a2a.Message, error) { + var data a2a.Message if err := json.Unmarshal([]byte(m.Data), &data); err != nil { - return protocol.Message{}, err + return a2a.Message{}, err } return data, nil } -func ParseMessages(messages []Event) ([]*protocol.Message, error) { - result := make([]*protocol.Message, 0, len(messages)) +func ParseMessages(messages []Event) ([]*a2a.Message, error) { + result := make([]*a2a.Message, 0, len(messages)) for _, message := range messages { parsed, err := message.Parse() if err != nil { @@ -77,24 +77,25 @@ type Session struct { } type Task struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at,omitempty"` - Data string `json:"data"` // JSON-serialized task data - SessionID string `json:"session_id"` + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + Data string `json:"data"` // JSON-serialized task data + ProtocolVersion *string `json:"protocol_version,omitempty"` + SessionID string `json:"session_id"` } -func (t *Task) Parse() (protocol.Task, error) { - var data protocol.Task +func (t *Task) Parse() (a2a.Task, error) { + var data a2a.Task if err := json.Unmarshal([]byte(t.Data), &data); err != nil { - return protocol.Task{}, err + return a2a.Task{}, err } return data, nil } -func ParseTasks(tasks []Task) ([]*protocol.Task, error) { - result := make([]*protocol.Task, 0, len(tasks)) +func ParseTasks(tasks []Task) ([]*a2a.Task, error) { + result := make([]*a2a.Task, 0, len(tasks)) for _, task := range tasks { parsed, err := task.Parse() if err != nil { @@ -106,12 +107,13 @@ func ParseTasks(tasks []Task) ([]*protocol.Task, error) { } type PushNotification struct { - ID string `json:"id"` - TaskID string `json:"task_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at,omitempty"` - Data string `json:"data"` // JSON-serialized push notification config + ID string `json:"id"` + TaskID string `json:"task_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + Data string `json:"data"` // JSON-serialized push notification config + ProtocolVersion *string `json:"protocol_version,omitempty"` } // FeedbackIssueType represents the category of feedback issue diff --git a/go/core/internal/a2a/a2a_handler_mux.go b/go/core/internal/a2a/a2a_handler_mux.go index 3d4bb7722e..fee3400a86 100644 --- a/go/core/internal/a2a/a2a_handler_mux.go +++ b/go/core/internal/a2a/a2a_handler_mux.go @@ -3,24 +3,27 @@ package a2a import ( "fmt" "net/http" + "slices" "strings" "sync" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2acompat/a2av0" + "github.com/a2aproject/a2a-go/v2/a2asrv" "github.com/gorilla/mux" authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" common "github.com/kagent-dev/kagent/go/core/internal/utils" "github.com/kagent-dev/kagent/go/core/pkg/auth" - "trpc.group/trpc-go/trpc-a2a-go/client" - "trpc.group/trpc-go/trpc-a2a-go/server" ) // A2AHandlerMux is an interface that defines methods for adding, getting, and removing agentic task handlers. type A2AHandlerMux interface { SetAgentHandler( agentRef string, - client *client.A2AClient, - card server.AgentCard, - tracing server.Middleware, + client *a2aclient.Client, + card a2atype.AgentCard, + tracing middleware, ) error RemoveAgentHandler( agentRef string, @@ -38,6 +41,10 @@ type handlerMux struct { var _ A2AHandlerMux = &handlerMux{} +type middleware interface { + Wrap(next http.Handler) http.Handler +} + func NewA2AHttpMux(agentPathPrefix, sandboxPathPrefix string, authenticator auth.AuthProvider) *handlerMux { return &handlerMux{ handlers: make(map[string]http.Handler), @@ -49,23 +56,48 @@ func NewA2AHttpMux(agentPathPrefix, sandboxPathPrefix string, authenticator auth func (a *handlerMux) SetAgentHandler( agentRef string, - client *client.A2AClient, - card server.AgentCard, - tracing server.Middleware, + client *a2aclient.Client, + card a2atype.AgentCard, + tracing middleware, ) error { - middlewares := []server.Middleware{authimpl.NewA2AAuthenticator(a.authenticator)} + // TODO(cleanup): Replace this protocol mux with the standard v1 handler stack once legacy clients/runtimes are unsupported. + requestHandler := NewPassthroughRequestHandler(client, &card) + legacyJSONRPCHandler := a2av0.NewJSONRPCHandler(requestHandler) + v1JSONRPCHandler := a2asrv.NewJSONRPCHandler(requestHandler) + cardHandler := a2asrv.NewAgentCardHandler(a2av0.NewStaticAgentCardProducer(&card)) + wellKnownPath := "/" + strings.TrimPrefix(a2asrv.WellKnownAgentCardPath, "/") + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, wellKnownPath) { + cardHandler.ServeHTTP(w, r) + return + } + wireVersion, err := common.NegotiateA2AWireVersion(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch wireVersion { + case common.A2AWireVersionLegacy: + legacyJSONRPCHandler.ServeHTTP(w, r) + case common.A2AWireVersionV1: + v1JSONRPCHandler.ServeHTTP(w, r) + default: + http.Error(w, fmt.Sprintf("unknown negotiated A2A wire version %q", wireVersion), http.StatusBadRequest) + } + }) + middlewares := []middleware{authimpl.NewA2AAuthenticator(a.authenticator)} if tracing != nil { middlewares = append(middlewares, tracing) } - srv, err := server.NewA2AServer(card, NewPassthroughManager(client), server.WithMiddleWare(middlewares...)) - if err != nil { - return fmt.Errorf("failed to create A2A server: %w", err) + for _, middleware := range slices.Backward(middlewares) { + handler = middleware.Wrap(handler) } a.lock.Lock() defer a.lock.Unlock() - a.handlers[agentRef] = srv.Handler() + a.handlers[agentRef] = handler return nil } diff --git a/go/core/internal/a2a/a2a_registrar.go b/go/core/internal/a2a/a2a_registrar.go index d21dbec040..f4d4133fe4 100644 --- a/go/core/internal/a2a/a2a_registrar.go +++ b/go/core/internal/a2a/a2a_registrar.go @@ -8,10 +8,12 @@ import ( "reflect" "time" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2acompat/a2av0" "github.com/go-logr/logr" "github.com/kagent-dev/kagent/go/api/v1alpha2" agent_translator "github.com/kagent-dev/kagent/go/core/internal/controller/translator/agent" - authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" common "github.com/kagent-dev/kagent/go/core/internal/utils" "github.com/kagent-dev/kagent/go/core/pkg/auth" "github.com/kagent-dev/kagent/go/core/pkg/env" @@ -20,16 +22,14 @@ import ( crcache "sigs.k8s.io/controller-runtime/pkg/cache" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" - a2aclient "trpc.group/trpc-go/trpc-a2a-go/client" ) type A2ARegistrar struct { - cache crcache.Cache - handlerMux A2AHandlerMux - a2aBaseURL string - sandboxA2AURL string - authenticator auth.AuthProvider - a2aBaseOptions []a2aclient.Option + cache crcache.Cache + handlerMux A2AHandlerMux + a2aBaseURL string + sandboxA2AURL string + authenticator auth.AuthProvider } var _ manager.Runnable = (*A2ARegistrar)(nil) @@ -40,9 +40,9 @@ func NewA2ARegistrar( a2aBaseUrl string, sandboxA2ABaseURL string, authenticator auth.AuthProvider, - streamingMaxBuf int, - streamingInitialBuf int, - streamingTimeout time.Duration, + _ int, + _ int, + _ time.Duration, ) *A2ARegistrar { reg := &A2ARegistrar{ cache: cache, @@ -50,11 +50,6 @@ func NewA2ARegistrar( a2aBaseURL: a2aBaseUrl, sandboxA2AURL: sandboxA2ABaseURL, authenticator: authenticator, - a2aBaseOptions: []a2aclient.Option{ - a2aclient.WithTimeout(streamingTimeout), - a2aclient.WithBuffer(streamingInitialBuf, streamingMaxBuf), - debugOpt(), - }, } return reg @@ -161,26 +156,35 @@ func (a *A2ARegistrar) upsertAgentHandler(ctx context.Context, agent v1alpha2.Ag provider := resolveProviderName(ctx, a.cache, agent) - client, err := a2aclient.NewA2AClient( - card.URL, - append( - a.a2aBaseOptions, - a2aclient.WithHTTPReqHandler( - &traceInjectHandler{ - next: authimpl.A2ARequestHandler( - a.authenticator, - agentRef, - ), - }, - ), - )..., + httpClient := debugHTTPClient() + client, err := a2aclient.NewFromEndpoints( + ctx, + // TODO(0.11.0): Prefer A2A 1.0 interfaces by default once managed runtimes are v1-capable. + // Keep legacy fallback during rollout so old agent pods continue to serve traffic. + filterInterfacesByVersion(card.SupportedInterfaces, a2atype.ProtocolVersion("0.3")), + a2aclient.WithJSONRPCTransport(httpClient), + // TODO(cleanup): Remove the compat transport after legacy runtimes are unsupported. + a2aclient.WithCompatTransport( + a2atype.ProtocolVersion("0.3"), + a2atype.TransportProtocolJSONRPC, + // This creates a legacy JSON-RPC transport that is used to forward traffic to agents that are still on the legacy A2A wire. + a2aclient.TransportFactoryFn(func(_ context.Context, _ *a2atype.AgentCard, iface *a2atype.AgentInterface) (a2aclient.Transport, error) { + return a2av0.NewJSONRPCTransport(a2av0.JSONRPCTransportConfig{ + URL: iface.URL, + Client: httpClient, + }), nil + }), + ), + a2aclient.WithCallInterceptors( + NewUpstreamAuthInterceptor(a.authenticator, agentRef), + ), ) if err != nil { return fmt.Errorf("create A2A client for %s: %w", agentRef, err) } cardCopy := *card - cardCopy.URL = a.a2aRouteURL(agent) + cardCopy.SupportedInterfaces = cloneInterfacesWithURL(card.SupportedInterfaces, a.a2aRouteURL(agent)) if err := a.handlerMux.SetAgentHandler(a2aRouteKey(agent), client, cardCopy, newA2ATracingMiddleware(agentRef, provider)); err != nil { return fmt.Errorf("set handler for %s: %w", agentRef, err) @@ -190,7 +194,7 @@ func (a *A2ARegistrar) upsertAgentHandler(ctx context.Context, agent v1alpha2.Ag return nil } -func debugOpt() a2aclient.Option { +func debugHTTPClient() *http.Client { debugAddr := env.KagentA2ADebugAddr.Get() if debugAddr != "" { client := new(http.Client) @@ -200,10 +204,9 @@ func debugOpt() a2aclient.Option { return zeroDialer.DialContext(ctx, network, debugAddr) }, } - return a2aclient.WithHTTPClient(client) - } else { - return func(*a2aclient.A2AClient) {} + return client } + return &http.Client{} } func (a *A2ARegistrar) a2aRouteURL(agent v1alpha2.AgentObject) string { @@ -222,3 +225,47 @@ func a2aRoutePath(agent v1alpha2.AgentObject) string { agentRef := types.NamespacedName{Namespace: agent.GetNamespace(), Name: agent.GetName()} return routeKey(agent.GetWorkloadMode() == v1alpha2.WorkloadModeSandbox, agentRef.Namespace, agentRef.Name) } + +// cloneInterfacesWithURL clones the interfaces and sets the URL to the given value. +func cloneInterfacesWithURL(interfaces []*a2atype.AgentInterface, url string) []*a2atype.AgentInterface { + if len(interfaces) == 0 { + return []*a2atype.AgentInterface{ + { + URL: url, + ProtocolBinding: a2atype.TransportProtocolJSONRPC, + ProtocolVersion: a2atype.Version, + }, + } + } + result := make([]*a2atype.AgentInterface, 0, len(interfaces)) + for _, i := range interfaces { + if i == nil { + continue + } + copied := *i + copied.URL = url + if copied.ProtocolVersion == "" { + copied.ProtocolVersion = a2atype.Version + } + result = append(result, &copied) + } + return result +} + +// filterInterfacesByVersion filters the interfaces to only include the ones that match the given version. +// Currently, this is used to select the A2A 0.3 interface for managed agents. +func filterInterfacesByVersion(interfaces []*a2atype.AgentInterface, version a2atype.ProtocolVersion) []*a2atype.AgentInterface { + filtered := make([]*a2atype.AgentInterface, 0, len(interfaces)) + for _, i := range interfaces { + if i == nil { + continue + } + if i.ProtocolVersion == version { + filtered = append(filtered, i) + } + } + if len(filtered) > 0 { + return filtered + } + return interfaces +} diff --git a/go/core/internal/a2a/a2a_utils.go b/go/core/internal/a2a/a2a_utils.go index bdd69663a8..99d90aa7d7 100644 --- a/go/core/internal/a2a/a2a_utils.go +++ b/go/core/internal/a2a/a2a_utils.go @@ -3,15 +3,18 @@ package a2a import ( "strings" - "trpc.group/trpc-go/trpc-a2a-go/protocol" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" ) // ExtractText extracts the text content from a message. -func ExtractText(message protocol.Message) string { +func ExtractText(message *a2atype.Message) string { + if message == nil { + return "" + } builder := strings.Builder{} for _, part := range message.Parts { - if textPart, ok := part.(*protocol.TextPart); ok { - builder.WriteString(textPart.Text) + if part != nil { + builder.WriteString(part.Text()) } } return builder.String() diff --git a/go/core/internal/a2a/client_interceptors.go b/go/core/internal/a2a/client_interceptors.go new file mode 100644 index 0000000000..c1800d14be --- /dev/null +++ b/go/core/internal/a2a/client_interceptors.go @@ -0,0 +1,66 @@ +package a2a + +import ( + "context" + "net/http" + + "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/kagent-dev/kagent/go/core/pkg/auth" + "go.opentelemetry.io/otel/propagation" + "k8s.io/apimachinery/pkg/types" +) + +type staticHeadersInterceptor struct { + a2aclient.PassthroughInterceptor + headers map[string]string +} + +func NewStaticHeadersInterceptor(headers map[string]string) a2aclient.CallInterceptor { + return &staticHeadersInterceptor{headers: headers} +} + +func (s *staticHeadersInterceptor) Before(ctx context.Context, req *a2aclient.Request) (context.Context, any, error) { + for k, v := range s.headers { + if v != "" { + req.ServiceParams.Append(k, v) + } + } + return ctx, nil, nil +} + +type upstreamAuthInterceptor struct { + a2aclient.PassthroughInterceptor + authProvider auth.AuthProvider + agentRef types.NamespacedName +} + +func NewUpstreamAuthInterceptor(authProvider auth.AuthProvider, agentRef types.NamespacedName) a2aclient.CallInterceptor { + return &upstreamAuthInterceptor{ + authProvider: authProvider, + agentRef: agentRef, + } +} + +func (u *upstreamAuthInterceptor) Before(ctx context.Context, req *a2aclient.Request) (context.Context, any, error) { + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, req.BaseURL, nil) + if err != nil { + return ctx, nil, err + } + if session, ok := auth.AuthSessionFrom(ctx); ok { + upstreamPrincipal := auth.Principal{ + Agent: auth.Agent{ + ID: u.agentRef.String(), + }, + } + if err := u.authProvider.UpstreamAuth(httpReq, session, upstreamPrincipal); err != nil { + return ctx, nil, err + } + } + propagation.TraceContext{}.Inject(ctx, propagation.HeaderCarrier(httpReq.Header)) + for k, values := range httpReq.Header { + for _, value := range values { + req.ServiceParams.Append(k, value) + } + } + return ctx, nil, nil +} diff --git a/go/core/internal/a2a/client_interceptors_test.go b/go/core/internal/a2a/client_interceptors_test.go new file mode 100644 index 0000000000..bf76addd6c --- /dev/null +++ b/go/core/internal/a2a/client_interceptors_test.go @@ -0,0 +1,56 @@ +package a2a + +import ( + "context" + "testing" + + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "k8s.io/apimachinery/pkg/types" +) + +func TestUpstreamAuthInterceptor_InjectsTraceContext(t *testing.T) { + const rawTraceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + + ctx := propagation.TraceContext{}.Extract( + context.Background(), + propagation.MapCarrier{"traceparent": rawTraceparent}, + ) + + req := &a2aclient.Request{ + BaseURL: "http://agent.default:8080", + ServiceParams: a2aclient.ServiceParams{}, + } + interceptor := NewUpstreamAuthInterceptor(nil, types.NamespacedName{}) + if _, _, err := interceptor.Before(ctx, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + gotValues := req.ServiceParams.Get("traceparent") + if len(gotValues) == 0 { + t.Fatal("expected traceparent service param on outgoing request, got none") + } + + outCtx := propagation.TraceContext{}.Extract(context.Background(), propagation.MapCarrier{"traceparent": gotValues[0]}) + wantTraceID := trace.SpanContextFromContext(ctx).TraceID() + gotTraceID := trace.SpanContextFromContext(outCtx).TraceID() + if wantTraceID != gotTraceID { + t.Errorf("trace ID: want %s, got %s", wantTraceID, gotTraceID) + } +} + +func TestUpstreamAuthInterceptor_NoTraceContext(t *testing.T) { + req := &a2aclient.Request{ + BaseURL: "http://agent.default:8080", + ServiceParams: a2aclient.ServiceParams{}, + } + interceptor := NewUpstreamAuthInterceptor(nil, types.NamespacedName{}) + if _, _, err := interceptor.Before(context.Background(), req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got := req.ServiceParams.Get("traceparent"); len(got) != 0 { + t.Errorf("expected no traceparent service param, got %q", got) + } +} diff --git a/go/core/internal/a2a/manager.go b/go/core/internal/a2a/manager.go index 97141e4a0e..29d0bb86d3 100644 --- a/go/core/internal/a2a/manager.go +++ b/go/core/internal/a2a/manager.go @@ -2,58 +2,46 @@ package a2a import ( "context" + "fmt" + "iter" - "trpc.group/trpc-go/trpc-a2a-go/client" - "trpc.group/trpc-go/trpc-a2a-go/protocol" - "trpc.group/trpc-go/trpc-a2a-go/taskmanager" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2asrv" ) -type PassthroughManager struct { - client *client.A2AClient +type PassthroughExecutor struct { + client *a2aclient.Client } -func NewPassthroughManager(client *client.A2AClient) taskmanager.TaskManager { - return &PassthroughManager{ +func NewPassthroughExecutor(client *a2aclient.Client) a2asrv.AgentExecutor { + return &PassthroughExecutor{ client: client, } } -func (m *PassthroughManager) OnSendMessage(ctx context.Context, request protocol.SendMessageParams) (*protocol.MessageResult, error) { - if request.Message.MessageID == "" { - request.Message.MessageID = protocol.GenerateMessageID() +func (m *PassthroughExecutor) Execute(ctx context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2atype.Event, error] { + return func(yield func(a2atype.Event, error) bool) { + if execCtx.Message == nil { + yield(nil, fmt.Errorf("missing message in executor context")) + return + } + req := &a2atype.SendMessageRequest{Message: execCtx.Message} + for event, err := range m.client.SendStreamingMessage(ctx, req) { + if !yield(event, err) { + return + } + } } - if request.Message.Kind == "" { - request.Message.Kind = protocol.KindMessage - } - return m.client.SendMessage(ctx, request) } -func (m *PassthroughManager) OnSendMessageStream(ctx context.Context, request protocol.SendMessageParams) (<-chan protocol.StreamingMessageEvent, error) { - if request.Message.MessageID == "" { - request.Message.MessageID = protocol.GenerateMessageID() - } - if request.Message.Kind == "" { - request.Message.Kind = protocol.KindMessage +func (m *PassthroughExecutor) Cancel(ctx context.Context, execCtx *a2asrv.ExecutorContext) iter.Seq2[a2atype.Event, error] { + return func(yield func(a2atype.Event, error) bool) { + task, err := m.client.CancelTask(ctx, &a2atype.CancelTaskRequest{ID: execCtx.TaskID}) + if err != nil { + yield(nil, err) + return + } + yield(task, nil) } - return m.client.StreamMessage(ctx, request) -} - -func (m *PassthroughManager) OnGetTask(ctx context.Context, params protocol.TaskQueryParams) (*protocol.Task, error) { - return m.client.GetTasks(ctx, params) -} - -func (m *PassthroughManager) OnCancelTask(ctx context.Context, params protocol.TaskIDParams) (*protocol.Task, error) { - return m.client.CancelTasks(ctx, params) -} - -func (m *PassthroughManager) OnPushNotificationSet(ctx context.Context, params protocol.TaskPushNotificationConfig) (*protocol.TaskPushNotificationConfig, error) { - return m.client.SetPushNotification(ctx, params) -} - -func (m *PassthroughManager) OnPushNotificationGet(ctx context.Context, params protocol.TaskIDParams) (*protocol.TaskPushNotificationConfig, error) { - return m.client.GetPushNotification(ctx, params) -} - -func (m *PassthroughManager) OnResubscribe(ctx context.Context, params protocol.TaskIDParams) (<-chan protocol.StreamingMessageEvent, error) { - return m.client.ResubscribeTask(ctx, params) } diff --git a/go/core/internal/a2a/passthrough_handler.go b/go/core/internal/a2a/passthrough_handler.go new file mode 100644 index 0000000000..3241b06460 --- /dev/null +++ b/go/core/internal/a2a/passthrough_handler.go @@ -0,0 +1,82 @@ +package a2a + +import ( + "context" + "iter" + + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2asrv" +) + +// TODO(cleanup): once legacy traffic is unsupported, use the standard v1 handler +// stack directly. +type PassthroughRequestHandler struct { + client *a2aclient.Client + card *a2atype.AgentCard +} + +var _ a2asrv.RequestHandler = (*PassthroughRequestHandler)(nil) + +// NewPassthroughRequestHandler returns a transport-level proxy for controller +// A2A endpoints. It delegates each request directly to the selected upstream +// agent client and intentionally bypasses a2asrv.NewHandler, which would create +// local task state and apply v1 task-processing invariants to legacy streams. +// Keep this while the controller bridges mixed 0.3 and 1.0 clients/runtimes; +func NewPassthroughRequestHandler(client *a2aclient.Client, card *a2atype.AgentCard) *PassthroughRequestHandler { + return &PassthroughRequestHandler{ + client: client, + card: card, + } +} + +func (h *PassthroughRequestHandler) GetTask(ctx context.Context, req *a2atype.GetTaskRequest) (*a2atype.Task, error) { + return h.client.GetTask(ctx, req) +} + +func (h *PassthroughRequestHandler) ListTasks(ctx context.Context, req *a2atype.ListTasksRequest) (*a2atype.ListTasksResponse, error) { + return h.client.ListTasks(ctx, req) +} + +func (h *PassthroughRequestHandler) CancelTask(ctx context.Context, req *a2atype.CancelTaskRequest) (*a2atype.Task, error) { + return h.client.CancelTask(ctx, req) +} + +func (h *PassthroughRequestHandler) SendMessage(ctx context.Context, req *a2atype.SendMessageRequest) (a2atype.SendMessageResult, error) { + return h.client.SendMessage(ctx, req) +} + +func (h *PassthroughRequestHandler) SubscribeToTask(ctx context.Context, req *a2atype.SubscribeToTaskRequest) iter.Seq2[a2atype.Event, error] { + return h.client.SubscribeToTask(ctx, req) +} + +func (h *PassthroughRequestHandler) SendStreamingMessage(ctx context.Context, req *a2atype.SendMessageRequest) iter.Seq2[a2atype.Event, error] { + return h.client.SendStreamingMessage(ctx, req) +} + +func (h *PassthroughRequestHandler) GetTaskPushConfig(ctx context.Context, req *a2atype.GetTaskPushConfigRequest) (*a2atype.PushConfig, error) { + return h.client.GetTaskPushConfig(ctx, req) +} + +func (h *PassthroughRequestHandler) ListTaskPushConfigs(ctx context.Context, req *a2atype.ListTaskPushConfigRequest) (*a2atype.ListTaskPushConfigResponse, error) { + configs, err := h.client.ListTaskPushConfigs(ctx, req) + if err != nil { + return nil, err + } + return &a2atype.ListTaskPushConfigResponse{Configs: configs}, nil +} + +func (h *PassthroughRequestHandler) CreateTaskPushConfig(ctx context.Context, req *a2atype.PushConfig) (*a2atype.PushConfig, error) { + return h.client.CreateTaskPushConfig(ctx, req) +} + +func (h *PassthroughRequestHandler) DeleteTaskPushConfig(ctx context.Context, req *a2atype.DeleteTaskPushConfigRequest) error { + return h.client.DeleteTaskPushConfig(ctx, req) +} + +func (h *PassthroughRequestHandler) GetExtendedAgentCard(ctx context.Context, req *a2atype.GetExtendedAgentCardRequest) (*a2atype.AgentCard, error) { + if h.card != nil && !h.card.Capabilities.ExtendedAgentCard { + return h.card, nil + } + return h.client.GetExtendedAgentCard(ctx, req) +} diff --git a/go/core/internal/a2a/trace.go b/go/core/internal/a2a/trace.go index bf8d7c94ad..943407e1d8 100644 --- a/go/core/internal/a2a/trace.go +++ b/go/core/internal/a2a/trace.go @@ -6,20 +6,18 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/types" crcache "sigs.k8s.io/controller-runtime/pkg/cache" - a2aclient "trpc.group/trpc-go/trpc-a2a-go/client" "github.com/kagent-dev/kagent/go/api/v1alpha2" ) // a2aTracingMiddleware is an A2A server middleware that creates an invoke_agent // span for each inbound A2A request, annotated with GenAI semantic convention -// attributes. The span becomes the parent of any outbound proxy calls made by -// traceInjectHandler, giving a clean agent-invocation span hierarchy in Jaeger. +// attributes. Outbound client interceptors inject that span into proxied agent +// calls, giving a clean agent-invocation span hierarchy in Jaeger. type a2aTracingMiddleware struct { agentRef types.NamespacedName provider attribute.KeyValue @@ -45,18 +43,6 @@ func (m *a2aTracingMiddleware) Wrap(next http.Handler) http.Handler { }) } -// traceInjectHandler wraps an HTTPReqHandler and injects W3C TraceContext -// headers (traceparent, tracestate) from the Go context into every outgoing -// proxy request, so the downstream agent receives the active span as its parent. -type traceInjectHandler struct { - next a2aclient.HTTPReqHandler -} - -func (h *traceInjectHandler) Handle(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { - propagation.TraceContext{}.Inject(ctx, propagation.HeaderCarrier(req.Header)) - return h.next.Handle(ctx, client, req) -} - // resolveProviderName looks up the ModelConfig for a declarative agent and // returns the corresponding gen_ai.provider.name attribute. Falls back to "kagent" // for BYO agents or if the ModelConfig cannot be fetched. diff --git a/go/core/internal/a2a/trace_test.go b/go/core/internal/a2a/trace_test.go index a7ee45c482..3e7c8de698 100644 --- a/go/core/internal/a2a/trace_test.go +++ b/go/core/internal/a2a/trace_test.go @@ -7,68 +7,12 @@ import ( "testing" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" - "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/types" ) -// mockHTTPReqHandler captures the request passed to Handle for inspection. -type mockHTTPReqHandler struct { - capturedReq *http.Request -} - -func (m *mockHTTPReqHandler) Handle(_ context.Context, _ *http.Client, req *http.Request) (*http.Response, error) { - m.capturedReq = req - return &http.Response{StatusCode: http.StatusOK}, nil -} - -func TestTraceInjectHandler_InjectsHeader(t *testing.T) { - const rawTraceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" - - ctx := propagation.TraceContext{}.Extract( - context.Background(), - propagation.MapCarrier{"traceparent": rawTraceparent}, - ) - - mock := &mockHTTPReqHandler{} - h := &traceInjectHandler{next: mock} - - req := httptest.NewRequest(http.MethodPost, "/", nil) - if _, err := h.Handle(ctx, nil, req); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - got := mock.capturedReq.Header.Get("traceparent") - if got == "" { - t.Fatal("expected traceparent header on outgoing request, got none") - } - - // The injected header must carry the same trace ID as the incoming context. - outCtx := propagation.TraceContext{}.Extract(context.Background(), propagation.HeaderCarrier(mock.capturedReq.Header)) - wantTraceID := trace.SpanContextFromContext(ctx).TraceID() - gotTraceID := trace.SpanContextFromContext(outCtx).TraceID() - if wantTraceID != gotTraceID { - t.Errorf("trace ID: want %s, got %s", wantTraceID, gotTraceID) - } -} - -func TestTraceInjectHandler_NoHeaderWhenNoTrace(t *testing.T) { - mock := &mockHTTPReqHandler{} - h := &traceInjectHandler{next: mock} - - req := httptest.NewRequest(http.MethodPost, "/", nil) - if _, err := h.Handle(context.Background(), nil, req); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if got := mock.capturedReq.Header.Get("traceparent"); got != "" { - t.Errorf("expected no traceparent header, got %q", got) - } -} - func TestA2ATracingMiddleware_SetsGenAIAttributes(t *testing.T) { exporter := tracetest.NewInMemoryExporter() tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter)) diff --git a/go/core/internal/controller/translator/agent/compiler.go b/go/core/internal/controller/translator/agent/compiler.go index 0232a859e6..c0f22eee8b 100644 --- a/go/core/internal/controller/translator/agent/compiler.go +++ b/go/core/internal/controller/translator/agent/compiler.go @@ -5,10 +5,10 @@ import ( "fmt" "slices" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/adk" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/utils" - "trpc.group/trpc-go/trpc-a2a-go/server" ) // AgentManifestInputs holds the translated data needed to emit Kubernetes resources. @@ -16,7 +16,7 @@ type AgentManifestInputs struct { Config *adk.AgentConfig Sandbox *v1alpha2.SandboxConfig Deployment *resolvedDeployment - AgentCard *server.AgentCard + AgentCard *a2a.AgentCard SecretHashBytes []byte } diff --git a/go/core/internal/controller/translator/agent/manifest_builder.go b/go/core/internal/controller/translator/agent/manifest_builder.go index 3b059bbef5..466edfe104 100644 --- a/go/core/internal/controller/translator/agent/manifest_builder.go +++ b/go/core/internal/controller/translator/agent/manifest_builder.go @@ -6,6 +6,9 @@ import ( "fmt" "maps" + a2a "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2acompat/a2av0" + "github.com/a2aproject/a2a-go/v2/a2asrv" "github.com/kagent-dev/kagent/go/api/adk" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/controller/translator/labels" @@ -18,7 +21,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "trpc.group/trpc-go/trpc-a2a-go/server" ) type manifestContext struct { @@ -57,7 +59,7 @@ func (a *adkApiTranslator) BuildManifest( outputs := &AgentOutputs{} manifestCtx := newManifestContext(agent, inputs.Deployment) - configSecret, err := a.buildConfigSecret(manifestCtx, inputs.Config, inputs.Sandbox, inputs.AgentCard, inputs.SecretHashBytes) + configSecret, err := a.buildConfigSecret(ctx, manifestCtx, inputs.Config, inputs.Sandbox, inputs.AgentCard, inputs.SecretHashBytes) if err != nil { return nil, err } @@ -125,10 +127,11 @@ func (m manifestContext) objectMeta() metav1.ObjectMeta { } func (a *adkApiTranslator) buildConfigSecret( + ctx context.Context, manifestCtx manifestContext, cfg *adk.AgentConfig, sandboxCfg *v1alpha2.SandboxConfig, - card *server.AgentCard, + card *a2a.AgentCard, modelConfigSecretHashBytes []byte, ) (*configSecretInputs, error) { cfgJSON := "" @@ -146,11 +149,17 @@ func (a *adkApiTranslator) buildConfigSecret( cfgJSON = string(bCfg) } if card != nil { - bCard, err := json.Marshal(card) + // TODO(0.11.0): use the v1 agent card producer once managed runtimes no longer need legacy top-level fields. + producer := a2av0.NewStaticAgentCardProducer(card) + jsonProducer, ok := producer.(a2asrv.AgentCardJSONProducer) + if !ok { + return nil, fmt.Errorf("compat agent card producer does not support JSON serialization") + } + cardJSON, err := jsonProducer.CardJSON(ctx) if err != nil { return nil, err } - agentCard = string(bCard) + agentCard = string(cardJSON) } if needsSRTSettings(manifestCtx.agent, sandboxCfg) { bSRTSettings, err := buildSRTSettingsJSON(sandboxCfg) diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_a2a_config.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_a2a_config.json index 3a467bf506..e2cb0e84b8 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_a2a_config.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_a2a_config.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,7 +11,6 @@ ], "description": "", "name": "a2a_agent", - "preferredTransport": "JSONRPC", "skills": [ { "description": "Summarizes text", @@ -21,7 +18,18 @@ "tags": null } ], - "url": "http://a2a-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://a2a-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://a2a-agent.test:8080" + } + ], "version": "" }, "config": { @@ -60,7 +68,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"a2a_agent\",\"description\":\"\",\"url\":\"http://a2a-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[{\"id\":\"summarize\",\"name\":\"Summarize\",\"description\":\"Summarizes text\",\"tags\":null}],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"a2a_agent\",\n \"version\": \"\",\n \"skills\": [\n {\n \"description\": \"Summarizes text\",\n \"id\": \"summarize\",\n \"name\": \"Summarize\",\n \"tags\": null\n }\n ],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://a2a-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://a2a-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://a2a-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -130,7 +138,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "16405455094195710426" + "kagent.dev/config-hash": "11777198347800362314" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json index 6c94d2f86d..acd88ecdc5 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent.test:8080" + } + ], "version": "" }, "config": { @@ -70,7 +78,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent\",\"description\":\"\",\"url\":\"http://agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"http_tools\":[{\"params\":{\"url\":\"http://mcp-server.test:8080/mcp\",\"headers\":{}},\"tools\":[\"tool1\",\"tool2\"],\"allowed_headers\":[\"x-user-email\",\"x-tenant-id\"]}],\"stream\":false}" } }, @@ -140,7 +148,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "5387783918115122395" + "kagent.dev/config-hash": "7055658815424891365" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_code.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_code.json index 597478474b..c959a47d6f 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_code.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_code.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_code", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-code.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-code.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-code.test:8080" + } + ], "version": "" }, "config": { @@ -62,7 +70,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_code\",\"description\":\"\",\"url\":\"http://agent-with-code.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_code\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-code.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-code.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-code.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"execute_code\":true,\"stream\":false}", "srt-settings.json": "{\"filesystem\":{\"allowWrite\":[\".\",\"/tmp\"],\"denyRead\":[],\"denyWrite\":[]},\"network\":{\"allowedDomains\":[],\"deniedDomains\":[]}}" } @@ -133,7 +141,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "17724008177186270332" + "kagent.dev/config-hash": "13638639139652067632" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_context_config.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_context_config.json index 1ac7356e6d..16b91cefdf 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_context_config.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_context_config.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "Agent with context management", "name": "agent_with_context", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-context.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-context.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-context.test:8080" + } + ], "version": "" }, "config": { @@ -72,7 +80,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_context\",\"description\":\"Agent with context management\",\"url\":\"http://agent-with-context.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"Agent with context management\",\n \"name\": \"agent_with_context\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-context.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-context.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-context.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"temperature\":0.7},\"description\":\"Agent with context management\",\"instruction\":\"You are a helpful assistant with context management enabled.\",\"stream\":false,\"context_config\":{\"compaction\":{\"compaction_interval\":5,\"overlap_size\":2,\"summarizer_model\":{\"type\":\"anthropic\",\"model\":\"claude-3-haiku\"},\"prompt_template\":\"Summarize the following conversation events concisely.\",\"token_threshold\":50000,\"event_retention_size\":10}}}" } }, @@ -142,7 +150,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "16891892632874106599" + "kagent.dev/config-hash": "7947347043949539668" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json index edaedd9719..283963cd96 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "An agent that uses cross-namespace tools", "name": "source_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://source-agent.source-ns:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://source-agent.source-ns:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://source-agent.source-ns:8080" + } + ], "version": "" }, "config": { @@ -76,7 +84,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"source_agent\",\"description\":\"An agent that uses cross-namespace tools\",\"url\":\"http://source-agent.source-ns:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"An agent that uses cross-namespace tools\",\n \"name\": \"source_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://source-agent.source-ns:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://source-agent.source-ns:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://source-agent.source-ns:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"An agent that uses cross-namespace tools\",\"instruction\":\"You are an assistant with access to shared tools.\",\"http_tools\":[{\"params\":{\"url\":\"http://tools.tools-ns.svc:8080/mcp\",\"headers\":{\"Authorization\":\"tool-secret-token\"},\"timeout\":30},\"tools\":[\"list_resources\",\"get_resource\"]}],\"remote_agents\":[{\"name\":\"tools_ns__NS__tools_agent\",\"url\":\"http://tools-agent.tools-ns:8080\",\"description\":\"An agent that can be used as a cross-namespace tool\"}],\"stream\":false}" } }, @@ -146,7 +154,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "6527476808794662414" + "kagent.dev/config-hash": "6928733945253389376" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json index d2ecefd3a6..26de14963f 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_custom_sa", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-custom-sa.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-custom-sa.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-custom-sa.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_custom_sa\",\"description\":\"\",\"url\":\"http://agent-with-custom-sa.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_custom_sa\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-custom-sa.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-custom-sa.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-custom-sa.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -99,7 +107,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "13887560842969010876" + "kagent.dev/config-hash": "4875306436828307681" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_default_sa.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_default_sa.json index 19548c5d01..1f8ba2ae2e 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_default_sa.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_default_sa.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_default_sa", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-default-sa.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-default-sa.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-default-sa.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_default_sa\",\"description\":\"\",\"url\":\"http://agent-with-default-sa.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_default_sa\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-default-sa.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-default-sa.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-default-sa.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -99,7 +107,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "8931708887213057908" + "kagent.dev/config-hash": "1952368464262477618" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_embedding_provider.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_embedding_provider.json index f2a9de95bf..d018d872cf 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_embedding_provider.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_embedding_provider.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_cross_provider_memory", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-cross-provider-memory.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-cross-provider-memory.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-cross-provider-memory.test:8080" + } + ], "version": "" }, "config": { @@ -60,7 +68,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_cross_provider_memory\",\"description\":\"\",\"url\":\"http://agent-cross-provider-memory.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_cross_provider_memory\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-cross-provider-memory.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-cross-provider-memory.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-cross-provider-memory.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an assistant.\",\"stream\":false,\"memory\":{\"embedding\":{\"provider\":\"gemini_vertex_ai\",\"model\":\"text-embedding-005\"}}}" } }, @@ -130,7 +138,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "10448813021284432574" + "kagent.dev/config-hash": "6685832133934502607" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_extra_containers.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_extra_containers.json index 6f9d1fa0bf..67a8e8a2f4 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_extra_containers.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_extra_containers.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_extra_containers", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-extra-containers.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-extra-containers.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-extra-containers.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_extra_containers\",\"description\":\"\",\"url\":\"http://agent-with-extra-containers.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_extra_containers\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-extra-containers.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-extra-containers.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-extra-containers.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -124,7 +132,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "6067225395749761191" + "kagent.dev/config-hash": "5942407443192129450" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json index 98cc97beab..aa4b77207a 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "git_skills_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://git-skills-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://git-skills-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://git-skills-agent.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"git_skills_agent\",\"description\":\"\",\"url\":\"http://git-skills-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"git_skills_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://git-skills-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://git-skills-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://git-skills-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant with skills from git.\",\"stream\":false}", "srt-settings.json": "{\"filesystem\":{\"allowWrite\":[\".\",\"/tmp\"],\"denyRead\":[],\"denyWrite\":[]},\"network\":{\"allowedDomains\":[],\"deniedDomains\":[]}}" } @@ -132,7 +140,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "9443054578640766875" + "kagent.dev/config-hash": "10177226668840890883" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json index 838ef2dbc2..bd5f97ca4a 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent.test:8080" + } + ], "version": "" }, "config": { @@ -69,7 +77,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent\",\"description\":\"\",\"url\":\"http://agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a math toolserver. Focus on solving mathematical problems step by step.\",\"http_tools\":[{\"params\":{\"url\":\"http://localhost:8084/mcp\",\"headers\":{\"MATH\":\"sk-test-api-key\"},\"timeout\":30,\"sse_read_timeout\":300},\"tools\":[\"k8s_get_resources\"]}],\"stream\":false}" } }, @@ -139,7 +147,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "4534869863837852487" + "kagent.dev/config-hash": "8643793714945604953" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json index 852ebeeafd..440b385bac 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent.test:8080" + } + ], "version": "" }, "config": { @@ -65,7 +73,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent\",\"description\":\"\",\"url\":\"http://agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a math toolserver. Focus on solving mathematical problems step by step.\",\"http_tools\":[{\"params\":{\"url\":\"http://toolserver.test:8084/mcp\",\"headers\":{}},\"tools\":[\"k8s_get_resources\"]}],\"stream\":false}" } }, @@ -135,7 +143,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "12595169530626754046" + "kagent.dev/config-hash": "90499429102488578" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_memory.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_memory.json index c8b70f08b4..940037c668 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_memory.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_memory.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_memory", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-memory.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-memory.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-memory.test:8080" + } + ], "version": "" }, "config": { @@ -65,7 +73,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_memory\",\"description\":\"\",\"url\":\"http://agent-with-memory.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_memory\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-memory.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-memory.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-memory.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"temperature\":0.7},\"description\":\"\",\"instruction\":\"You are a helpful assistant with memory. Save important findings and use past context when relevant.\",\"stream\":false,\"memory\":{\"embedding\":{\"provider\":\"openai\",\"model\":\"text-embedding-3-small\"}}}" } }, @@ -135,7 +143,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "11340664467578377382" + "kagent.dev/config-hash": "4675326514170066600" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json index caba32bec9..4183fe912c 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "parent_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://parent-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://parent-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://parent-agent.test:8080" + } + ], "version": "" }, "config": { @@ -63,7 +71,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"parent_agent\",\"description\":\"\",\"url\":\"http://parent-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"parent_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://parent-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://parent-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://parent-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are a coordinating agent that can delegate tasks to specialists.\",\"remote_agents\":[{\"name\":\"test__NS__specialist_agent\",\"url\":\"http://specialist-agent.test:8080\",\"headers\":{\"FOO\":\"sup3rs3cr3t\"}}],\"stream\":false}" } }, @@ -133,7 +141,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "963887663212034528" + "kagent.dev/config-hash": "12309204394800027371" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json index 0a9a7fcda8..2cf6122979 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "passthrough_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://passthrough-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://passthrough-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://passthrough-agent.test:8080" + } + ], "version": "" }, "config": { @@ -56,7 +64,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"passthrough_agent\",\"description\":\"\",\"url\":\"http://passthrough-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"passthrough_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://passthrough-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://passthrough-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://passthrough-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"api_key_passthrough\":true,\"base_url\":\"\",\"temperature\":0.7},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -126,7 +134,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "2436412068147442351" + "kagent.dev/config-hash": "15376076214432010811" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json index e13ba958fe..8a7f398b70 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_prompt_template.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "A Kubernetes troubleshooting agent", "name": "agent_with_prompt_template", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-prompt-template.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-prompt-template.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-prompt-template.test:8080" + } + ], "version": "" }, "config": { @@ -67,7 +75,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_prompt_template\",\"description\":\"A Kubernetes troubleshooting agent\",\"url\":\"http://agent-with-prompt-template.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"A Kubernetes troubleshooting agent\",\n \"name\": \"agent_with_prompt_template\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-prompt-template.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-prompt-template.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-prompt-template.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"A Kubernetes troubleshooting agent\",\"instruction\":\"## Preamble\\nYou are a helpful Kubernetes assistant.\\n\\n\\nYou are agent-with-prompt-template, operating in test.\\nYour purpose: A Kubernetes troubleshooting agent\\n\\nAvailable tools: k8s_get_resources, k8s_describe_resource, \\n\\n## Safety Guidelines\\nNever delete resources without explicit user confirmation.\\n\\n\",\"http_tools\":[{\"params\":{\"url\":\"http://localhost:8084/mcp\",\"headers\":{},\"timeout\":30},\"tools\":[\"k8s_get_resources\",\"k8s_describe_resource\"]}],\"stream\":false}" } }, @@ -137,7 +145,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "17922600608856796760" + "kagent.dev/config-hash": "2608374849770441844" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json index 901ce90339..666a1fb10b 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_proxy", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-proxy.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-proxy.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-proxy.test:8080" + } + ], "version": "" }, "config": { @@ -76,7 +84,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_proxy\",\"description\":\"\",\"url\":\"http://agent-with-proxy.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_proxy\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-proxy.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-proxy.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-proxy.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"test-mcp-server.kagent\"}},\"tools\":[\"test-tool\"]}],\"remote_agents\":[{\"name\":\"test__NS__nested_agent\",\"url\":\"http://proxy.kagent.svc.cluster.local:8080\",\"headers\":{\"x-kagent-host\":\"nested-agent.test\"}}],\"stream\":false}" } }, @@ -146,7 +154,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "1327907664326337797" + "kagent.dev/config-hash": "6086147857331851037" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json index 53cf7f4bc7..1dab97f4c6 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_proxy_external", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-proxy-external.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-proxy-external.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-proxy-external.test:8080" + } + ], "version": "" }, "config": { @@ -65,7 +73,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_proxy_external\",\"description\":\"\",\"url\":\"http://agent-with-proxy-external.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_proxy_external\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-proxy-external.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-proxy-external.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-proxy-external.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"https://external-mcp.example.com/mcp\",\"headers\":{}},\"tools\":[\"test-tool\"]}],\"stream\":false}" } }, @@ -135,7 +143,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "8764617675044151097" + "kagent.dev/config-hash": "16116210188225830835" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json index e80a0bf78e..ce1409ab7c 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_proxy_mcpserver", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-proxy-mcpserver.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-proxy-mcpserver.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-proxy-mcpserver.test:8080" + } + ], "version": "" }, "config": { @@ -68,7 +76,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_proxy_mcpserver\",\"description\":\"\",\"url\":\"http://agent-with-proxy-mcpserver.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_proxy_mcpserver\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-proxy-mcpserver.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-proxy-mcpserver.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-proxy-mcpserver.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"test-mcp-server.test\"},\"timeout\":30},\"tools\":[\"test-tool\"]}],\"stream\":false}" } }, @@ -138,7 +146,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "17237679247457206561" + "kagent.dev/config-hash": "699819535100768323" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json index 150bc253d0..66199196a8 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_proxy_mcpserver_timeout", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-proxy-mcpserver-timeout.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-proxy-mcpserver-timeout.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-proxy-mcpserver-timeout.test:8080" + } + ], "version": "" }, "config": { @@ -68,7 +76,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_proxy_mcpserver_timeout\",\"description\":\"\",\"url\":\"http://agent-with-proxy-mcpserver-timeout.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_proxy_mcpserver_timeout\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-proxy-mcpserver-timeout.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-proxy-mcpserver-timeout.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-proxy-mcpserver-timeout.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"test-mcp-server.test\"},\"timeout\":60},\"tools\":[\"test-tool\"]}],\"stream\":false}" } }, @@ -138,7 +146,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "7011830610500130414" + "kagent.dev/config-hash": "5660639111005887260" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json index 0010bc16d9..4a03d166d1 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_proxy_service", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-proxy-service.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-proxy-service.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-proxy-service.test:8080" + } + ], "version": "" }, "config": { @@ -67,7 +75,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_proxy_service\",\"description\":\"\",\"url\":\"http://agent-with-proxy-service.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_proxy_service\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-proxy-service.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-proxy-service.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-proxy-service.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"toolserver.test\"}},\"tools\":[\"k8s_get_resources\"]}],\"stream\":false}" } }, @@ -137,7 +145,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "17657014285076291438" + "kagent.dev/config-hash": "2741762687963053248" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_require_approval.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_require_approval.json index e48937b0f2..e592026a1e 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_require_approval.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_require_approval.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent.test:8080" + } + ], "version": "" }, "config": { @@ -71,7 +79,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent\",\"description\":\"\",\"url\":\"http://agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You help users manage files.\",\"http_tools\":[{\"params\":{\"url\":\"http://toolserver.test:8084/mcp\",\"headers\":{}},\"tools\":[\"read_file\",\"write_file\",\"delete_file\"],\"require_approval\":[\"delete_file\",\"write_file\"]}],\"stream\":false}" } }, @@ -141,7 +149,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "2233830583481326333" + "kagent.dev/config-hash": "8217777934714265853" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json index c1f32a0921..fd9524ce33 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_scheduling_attributes", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-scheduling-attributes.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-scheduling-attributes.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-scheduling-attributes.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_scheduling_attributes\",\"description\":\"\",\"url\":\"http://agent-with-scheduling-attributes.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_scheduling_attributes\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-scheduling-attributes.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-scheduling-attributes.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-scheduling-attributes.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -131,7 +139,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "7052837506672819306" + "kagent.dev/config-hash": "1301028021327278116" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json index b1372c0b3b..028a009568 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_security_context", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-security-context.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-security-context.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-security-context.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_security_context\",\"description\":\"\",\"url\":\"http://agent-with-security-context.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_security_context\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-security-context.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-security-context.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-security-context.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -131,7 +139,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "3665823864130830967" + "kagent.dev/config-hash": "3654896060561973296" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index e65fa19d57..91f1fe968d 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "skills_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://skills-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://skills-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://skills-agent.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"skills_agent\",\"description\":\"\",\"url\":\"http://skills-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"skills_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://skills-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://skills-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://skills-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}", "srt-settings.json": "{\"filesystem\":{\"allowWrite\":[\".\",\"/tmp\"],\"denyRead\":[],\"denyWrite\":[]},\"network\":{\"allowedDomains\":[],\"deniedDomains\":[]}}" } @@ -132,7 +140,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "4496754913718271849" + "kagent.dev/config-hash": "1709213079642646235" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json index e0d08cd124..ccc3ddc1c0 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "basic_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://basic-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://basic-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://basic-agent.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"basic_agent\",\"description\":\"\",\"url\":\"http://basic-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"basic_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":true}" } }, @@ -131,7 +139,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "4834502221619930746" + "kagent.dev/config-hash": "7925219030441964607" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json index de6ed10545..a8ad8d0127 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_configmap_system_message", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-configmap-system-message.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-configmap-system-message.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-configmap-system-message.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_configmap_system_message\",\"description\":\"\",\"url\":\"http://agent-with-configmap-system-message.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_configmap_system_message\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-configmap-system-message.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-configmap-system-message.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-configmap-system-message.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"Speak in the style of Shakespeare.\",\"stream\":false}" } }, @@ -124,7 +132,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "6732733949178246074" + "kagent.dev/config-hash": "14676988129172891564" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json index a03bbbea85..afb1453bec 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "agent_with_secret_system_message", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://agent-with-secret-system-message.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://agent-with-secret-system-message.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://agent-with-secret-system-message.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"agent_with_secret_system_message\",\"description\":\"\",\"url\":\"http://agent-with-secret-system-message.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"agent_with_secret_system_message\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://agent-with-secret-system-message.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://agent-with-secret-system-message.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://agent-with-secret-system-message.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You will speak in the style of Shakespeare.\\n\",\"stream\":false}" } }, @@ -124,7 +132,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "1798322143389478148" + "kagent.dev/config-hash": "13184379951838451524" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json b/go/core/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json index b17be6ab43..abeecdbb99 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "anthropic_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://anthropic-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://anthropic-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://anthropic-agent.test:8080" + } + ], "version": "" }, "config": { @@ -53,7 +61,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"anthropic_agent\",\"description\":\"\",\"url\":\"http://anthropic-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"anthropic_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://anthropic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://anthropic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://anthropic-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"anthropic\",\"model\":\"claude-3-sonnet-20240229\"},\"description\":\"\",\"instruction\":\"You are Claude, an AI assistant created by Anthropic.\",\"stream\":false}" } }, @@ -123,7 +131,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "12361184581110359614" + "kagent.dev/config-hash": "1266575541990203530" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/basic_agent.json b/go/core/internal/controller/translator/agent/testdata/outputs/basic_agent.json index 81915807b9..af7e049ef8 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/basic_agent.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/basic_agent.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "basic_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://basic-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://basic-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://basic-agent.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"basic_agent\",\"description\":\"\",\"url\":\"http://basic-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"basic_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://basic-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant.\",\"stream\":false}" } }, @@ -131,7 +139,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "7118705820066737230" + "kagent.dev/config-hash": "13011412982718834568" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json b/go/core/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json index c610f58ca2..7f30e79f06 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "bedrock_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://bedrock-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://bedrock-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://bedrock-agent.test:8080" + } + ], "version": "" }, "config": { @@ -54,7 +62,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"bedrock_agent\",\"description\":\"\",\"url\":\"http://bedrock-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"bedrock_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://bedrock-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://bedrock-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://bedrock-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"bedrock\",\"model\":\"us.anthropic.claude-sonnet-4-20250514-v1:0\",\"region\":\"us-east-1\"},\"description\":\"\",\"instruction\":\"You are a helpful AI assistant running on AWS Bedrock.\",\"stream\":false}" } }, @@ -124,7 +132,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "11640375396581880325" + "kagent.dev/config-hash": "10296132164457756348" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/ollama_agent.json b/go/core/internal/controller/translator/agent/testdata/outputs/ollama_agent.json index 02a0ddae91..409e5d175d 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/ollama_agent.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/ollama_agent.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "ollama_agent", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://ollama-agent.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://ollama-agent.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://ollama-agent.test:8080" + } + ], "version": "" }, "config": { @@ -61,7 +69,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"ollama_agent\",\"description\":\"\",\"url\":\"http://ollama-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"ollama_agent\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://ollama-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://ollama-agent.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://ollama-agent.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"ollama\",\"model\":\"llama3.2:latest\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"options\":{\"num_ctx\":\"2048\",\"temperature\":\"0.8\",\"top_p\":\"0.9\"}},\"description\":\"\",\"instruction\":\"You are a helpful AI assistant running locally via Ollama.\",\"stream\":false}" } }, @@ -131,7 +139,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "1298797653455180865" + "kagent.dev/config-hash": "16198809549325160576" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json index 62e996c083..2039cad5c1 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "tls_agent_with_custom_ca", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://tls-agent-with-custom-ca.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://tls-agent-with-custom-ca.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://tls-agent-with-custom-ca.test:8080" + } + ], "version": "" }, "config": { @@ -59,7 +67,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"tls_agent_with_custom_ca\",\"description\":\"\",\"url\":\"http://tls-agent-with-custom-ca.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"tls_agent_with_custom_ca\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://tls-agent-with-custom-ca.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://tls-agent-with-custom-ca.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://tls-agent-with-custom-ca.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"tls_insecure_skip_verify\":false,\"tls_ca_cert_path\":\"/etc/ssl/certs/custom/ca.crt\",\"tls_disable_system_cas\":false,\"base_url\":\"https://internal-litellm.company.com\",\"max_tokens\":1024,\"temperature\":0.7},\"description\":\"\",\"instruction\":\"You are a helpful assistant with custom CA support.\",\"stream\":false}" } }, @@ -129,7 +137,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "7617586729040135348" + "kagent.dev/config-hash": "1274635499647008262" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json index 5da58bfdcd..1eebea8aff 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "tls_agent_with_disabled_verify", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://tls-agent-with-disabled-verify.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://tls-agent-with-disabled-verify.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://tls-agent-with-disabled-verify.test:8080" + } + ], "version": "" }, "config": { @@ -58,7 +66,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"tls_agent_with_disabled_verify\",\"description\":\"\",\"url\":\"http://tls-agent-with-disabled-verify.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"tls_agent_with_disabled_verify\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://tls-agent-with-disabled-verify.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://tls-agent-with-disabled-verify.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://tls-agent-with-disabled-verify.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"tls_insecure_skip_verify\":true,\"tls_disable_system_cas\":false,\"base_url\":\"https://dev-litellm.local\",\"max_tokens\":1024,\"temperature\":0.7},\"description\":\"\",\"instruction\":\"You are a helpful assistant in a development environment.\",\"stream\":false}" } }, @@ -128,7 +136,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "11053867675743064086" + "kagent.dev/config-hash": "12364563615889863360" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json index 620c29c9f5..2dbb8c1eed 100644 --- a/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json +++ b/go/core/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json @@ -1,8 +1,6 @@ { "agentCard": { "capabilities": { - "pushNotifications": false, - "stateTransitionHistory": true, "streaming": true }, "defaultInputModes": [ @@ -13,9 +11,19 @@ ], "description": "", "name": "tls_agent_with_system_cas_disabled", - "preferredTransport": "JSONRPC", "skills": null, - "url": "http://tls-agent-with-system-cas-disabled.test:8080", + "supportedInterfaces": [ + { + "protocolBinding": "JSONRPC", + "protocolVersion": "0.3", + "url": "http://tls-agent-with-system-cas-disabled.test:8080" + }, + { + "protocolBinding": "JSONRPC", + "protocolVersion": "1.0", + "url": "http://tls-agent-with-system-cas-disabled.test:8080" + } + ], "version": "" }, "config": { @@ -59,7 +67,7 @@ ] }, "stringData": { - "agent-card.json": "{\"name\":\"tls_agent_with_system_cas_disabled\",\"description\":\"\",\"url\":\"http://tls-agent-with-system-cas-disabled.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[],\"preferredTransport\":\"JSONRPC\"}", + "agent-card.json": "{\n \"defaultInputModes\": [\n \"text\"\n ],\n \"defaultOutputModes\": [\n \"text\"\n ],\n \"description\": \"\",\n \"name\": \"tls_agent_with_system_cas_disabled\",\n \"version\": \"\",\n \"skills\": [],\n \"capabilities\": {\n \"streaming\": true\n },\n \"supportedInterfaces\": [\n {\n \"url\": \"http://tls-agent-with-system-cas-disabled.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"0.3\"\n },\n {\n \"url\": \"http://tls-agent-with-system-cas-disabled.test:8080\",\n \"protocolBinding\": \"JSONRPC\",\n \"protocolVersion\": \"1.0\"\n }\n ],\n \"url\": \"http://tls-agent-with-system-cas-disabled.test:8080\",\n \"protocolVersion\": \"0.3\",\n \"preferredTransport\": \"JSONRPC\"\n}", "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"tls_insecure_skip_verify\":false,\"tls_ca_cert_path\":\"/etc/ssl/certs/custom/ca.crt\",\"tls_disable_system_cas\":true,\"base_url\":\"https://corp-llm-gateway.internal\",\"max_tokens\":1024,\"temperature\":0.7},\"description\":\"\",\"instruction\":\"You are a helpful assistant in a corporate environment.\",\"stream\":false}" } }, @@ -129,7 +137,7 @@ "template": { "metadata": { "annotations": { - "kagent.dev/config-hash": "1527591075664209989" + "kagent.dev/config-hash": "16193366991679050633" }, "labels": { "app": "kagent", diff --git a/go/core/internal/controller/translator/agent/utils.go b/go/core/internal/controller/translator/agent/utils.go index 83b9bbd680..e633cc031d 100644 --- a/go/core/internal/controller/translator/agent/utils.go +++ b/go/core/internal/controller/translator/agent/utils.go @@ -2,38 +2,58 @@ package agent import ( "fmt" - "slices" "strings" - a2atype "github.com/a2aproject/a2a-go/a2a" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/v1alpha2" - "github.com/kagent-dev/kagent/go/core/internal/utils" - "trpc.group/trpc-go/trpc-a2a-go/server" ) -func GetA2AAgentCard(agent v1alpha2.AgentObject) *server.AgentCard { +func GetA2AAgentCard(agent v1alpha2.AgentObject) *a2atype.AgentCard { spec := agent.GetAgentSpec() - preferredTransport := string(a2atype.TransportProtocolJSONRPC) - card := server.AgentCard{ - Name: strings.ReplaceAll(agent.GetName(), "-", "_"), - Description: spec.Description, - URL: fmt.Sprintf("http://%s.%s:8080", agent.GetName(), agent.GetNamespace()), - PreferredTransport: &preferredTransport, - Capabilities: server.AgentCapabilities{ - Streaming: new(true), - PushNotifications: new(false), - StateTransitionHistory: new(true), + card := a2atype.AgentCard{ + Name: strings.ReplaceAll(agent.GetName(), "-", "_"), + Description: spec.Description, + SupportedInterfaces: []*a2atype.AgentInterface{ + { + URL: fmt.Sprintf("http://%s.%s:8080", agent.GetName(), agent.GetNamespace()), + ProtocolBinding: a2atype.TransportProtocolJSONRPC, + ProtocolVersion: a2atype.ProtocolVersion("0.3"), + }, + { + URL: fmt.Sprintf("http://%s.%s:8080", agent.GetName(), agent.GetNamespace()), + ProtocolBinding: a2atype.TransportProtocolJSONRPC, + ProtocolVersion: a2atype.Version, + }, }, - // Can't be null for Python, so set to empty list - Skills: []server.AgentSkill{}, + Capabilities: a2atype.AgentCapabilities{ + Streaming: true, + PushNotifications: false, + }, + // Can't be null for Python, so set to empty list. + Skills: []a2atype.AgentSkill{}, DefaultInputModes: []string{"text"}, DefaultOutputModes: []string{"text"}, } if spec.Type == v1alpha2.AgentType_Declarative && spec.Declarative != nil && spec.Declarative.A2AConfig != nil { - decl := spec.Declarative - card.Skills = slices.Collect(utils.Map(slices.Values(decl.A2AConfig.Skills), func(skill v1alpha2.AgentSkill) server.AgentSkill { - return server.AgentSkill(skill) - })) + card.Skills = make([]a2atype.AgentSkill, 0, len(spec.Declarative.A2AConfig.Skills)) + for _, skill := range spec.Declarative.A2AConfig.Skills { + card.Skills = append(card.Skills, a2atype.AgentSkill{ + ID: skill.ID, + Name: skill.Name, + Description: derefString(skill.Description), + Tags: skill.Tags, + Examples: skill.Examples, + InputModes: skill.InputModes, + OutputModes: skill.OutputModes, + }) + } } return &card } + +func derefString(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/go/core/internal/controller/translator/agent/utils_test.go b/go/core/internal/controller/translator/agent/utils_test.go index 5cdc2b3db5..61dd6af647 100644 --- a/go/core/internal/controller/translator/agent/utils_test.go +++ b/go/core/internal/controller/translator/agent/utils_test.go @@ -3,11 +3,10 @@ package agent_test import ( "testing" - a2atype "github.com/a2aproject/a2a-go/a2a" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "trpc.group/trpc-go/trpc-a2a-go/server" "github.com/kagent-dev/kagent/go/api/v1alpha2" translator "github.com/kagent-dev/kagent/go/core/internal/controller/translator/agent" @@ -20,7 +19,7 @@ func TestGetA2AAgentCard(t *testing.T) { wantName string wantDescription string wantURL string - wantSkills []server.AgentSkill + wantSkills []a2atype.AgentSkill }{ { name: "declarative agent with a2a config and skills", @@ -45,7 +44,7 @@ func TestGetA2AAgentCard(t *testing.T) { wantName: "test_agent", wantDescription: "A test agent", wantURL: "http://test-agent.default:8080", - wantSkills: []server.AgentSkill{{Name: "skill-1"}, {Name: "skill-2"}}, + wantSkills: []a2atype.AgentSkill{{Name: "skill-1"}, {Name: "skill-2"}}, }, { name: "declarative agent with nil declarative spec", @@ -62,7 +61,7 @@ func TestGetA2AAgentCard(t *testing.T) { wantName: "nil_declarative", wantDescription: "", wantURL: "http://nil-declarative.default:8080", - wantSkills: []server.AgentSkill{}, + wantSkills: []a2atype.AgentSkill{}, }, { name: "declarative agent with nil a2a config", @@ -81,7 +80,7 @@ func TestGetA2AAgentCard(t *testing.T) { wantName: "no_a2a", wantDescription: "", wantURL: "http://no-a2a.default:8080", - wantSkills: []server.AgentSkill{}, + wantSkills: []a2atype.AgentSkill{}, }, { name: "BYO agent", @@ -97,7 +96,7 @@ func TestGetA2AAgentCard(t *testing.T) { wantName: "byo_agent", wantDescription: "", wantURL: "http://byo-agent.default:8080", - wantSkills: []server.AgentSkill{}, + wantSkills: []a2atype.AgentSkill{}, }, } @@ -108,15 +107,18 @@ func TestGetA2AAgentCard(t *testing.T) { assert.NotNil(t, card) assert.Equal(t, tt.wantName, card.Name) assert.Equal(t, tt.wantDescription, card.Description) - assert.Equal(t, tt.wantURL, card.URL) + require.Len(t, card.SupportedInterfaces, 2) + assert.Equal(t, tt.wantURL, card.SupportedInterfaces[0].URL) + assert.Equal(t, a2atype.TransportProtocolJSONRPC, card.SupportedInterfaces[0].ProtocolBinding) + assert.Equal(t, a2atype.ProtocolVersion("0.3"), card.SupportedInterfaces[0].ProtocolVersion) + assert.Equal(t, tt.wantURL, card.SupportedInterfaces[1].URL) + assert.Equal(t, a2atype.TransportProtocolJSONRPC, card.SupportedInterfaces[1].ProtocolBinding) + assert.Equal(t, a2atype.Version, card.SupportedInterfaces[1].ProtocolVersion) assert.Equal(t, tt.wantSkills, card.Skills) assert.Equal(t, []string{"text"}, card.DefaultInputModes) assert.Equal(t, []string{"text"}, card.DefaultOutputModes) - require.NotNil(t, card.PreferredTransport) - assert.Equal(t, string(a2atype.TransportProtocolJSONRPC), *card.PreferredTransport) - assert.True(t, *card.Capabilities.Streaming) - assert.False(t, *card.Capabilities.PushNotifications) - assert.True(t, *card.Capabilities.StateTransitionHistory) + assert.True(t, card.Capabilities.Streaming) + assert.False(t, card.Capabilities.PushNotifications) }) } } diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index 15a2dbacd7..01ab1b7067 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -8,11 +8,13 @@ import ( "strings" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" dbpkg "github.com/kagent-dev/kagent/go/api/database" "github.com/kagent-dev/kagent/go/api/v1alpha2" dbgen "github.com/kagent-dev/kagent/go/core/internal/database/gen" + "github.com/kagent-dev/kagent/go/core/pkg/a2acompat/trpcv0" "github.com/pgvector/pgvector-go" "trpc.group/trpc-go/trpc-a2a-go/protocol" ) @@ -197,40 +199,47 @@ func (c *postgresClient) ListEventsForSession(ctx context.Context, sessionID, us // ── Tasks ───────────────────────────────────────────────────────────────────── -func (c *postgresClient) StoreTask(ctx context.Context, task *protocol.Task) error { - data, err := json.Marshal(task) +// TODO(0.11.0): Switch task writes to v1 storage format and remove legacy conversion from this write path. +// NOTE: We will still need to keep the read compatibility for legacy rows in 0.11.0 +func (c *postgresClient) StoreTask(ctx context.Context, task *a2a.Task) error { + legacyTask, err := trpcv0.ToLegacyTask(task) + if err != nil { + return fmt.Errorf("failed to convert task to legacy format: %w", err) + } + data, err := json.Marshal(legacyTask) if err != nil { return fmt.Errorf("failed to serialize task: %w", err) } return c.q.UpsertTask(ctx, dbgen.UpsertTaskParams{ - ID: task.ID, - Data: string(data), - SessionID: strPtrIfNotEmpty(task.ContextID), + ID: string(task.ID), + Data: string(data), + SessionID: strPtrIfNotEmpty(task.ContextID), + ProtocolVersion: nil, }) } -func (c *postgresClient) GetTask(ctx context.Context, taskID string) (*protocol.Task, error) { +func (c *postgresClient) GetTask(ctx context.Context, taskID string) (*a2a.Task, error) { row, err := c.q.GetTask(ctx, taskID) if err != nil { return nil, fmt.Errorf("failed to get task %s: %w", taskID, err) } - var task protocol.Task - if err := json.Unmarshal([]byte(row.Data), &task); err != nil { - return nil, fmt.Errorf("failed to deserialize task: %w", err) - } - return &task, nil + return parseVersionedTask(row.Data, row.ProtocolVersion) } -func (c *postgresClient) ListTasksForSession(ctx context.Context, sessionID string) ([]*protocol.Task, error) { +func (c *postgresClient) ListTasksForSession(ctx context.Context, sessionID string) ([]*a2a.Task, error) { rows, err := c.q.ListTasksForSession(ctx, &sessionID) if err != nil { return nil, fmt.Errorf("failed to list tasks for session: %w", err) } - tasks := make([]dbpkg.Task, len(rows)) + tasks := make([]*a2a.Task, 0, len(rows)) for i, r := range rows { - tasks[i] = *toTask(r) + task, err := parseVersionedTask(r.Data, r.ProtocolVersion) + if err != nil { + return nil, fmt.Errorf("failed to parse task row %d: %w", i, err) + } + tasks = append(tasks, task) } - return dbpkg.ParseTasks(tasks) + return tasks, nil } func (c *postgresClient) DeleteTask(ctx context.Context, taskID string) error { @@ -239,42 +248,42 @@ func (c *postgresClient) DeleteTask(ctx context.Context, taskID string) error { // ── Push Notifications ──────────────────────────────────────────────────────── -func (c *postgresClient) StorePushNotification(ctx context.Context, config *protocol.TaskPushNotificationConfig) error { - data, err := json.Marshal(config) +// TODO(0.11.0): Switch push notification writes to v1 storage format and remove legacy conversion from this write path. +// NOTE: We will still need to keep the read compatibility for legacy rows in 0.11.0. +func (c *postgresClient) StorePushNotification(ctx context.Context, config *a2a.PushConfig) error { + legacyConfig := trpcv0.ToLegacyPushConfig(config) + data, err := json.Marshal(legacyConfig) if err != nil { return fmt.Errorf("failed to serialize push notification: %w", err) } return c.q.UpsertPushNotification(ctx, dbgen.UpsertPushNotificationParams{ - ID: config.PushNotificationConfig.ID, - TaskID: config.TaskID, - Data: string(data), + ID: config.ID, + TaskID: string(config.TaskID), + Data: string(data), + ProtocolVersion: nil, }) } -func (c *postgresClient) GetPushNotification(ctx context.Context, taskID, configID string) (*protocol.TaskPushNotificationConfig, error) { +func (c *postgresClient) GetPushNotification(ctx context.Context, taskID, configID string) (*a2a.PushConfig, error) { row, err := c.q.GetPushNotification(ctx, dbgen.GetPushNotificationParams{TaskID: taskID, ID: configID}) if err != nil { return nil, fmt.Errorf("failed to get push notification: %w", err) } - var cfg protocol.TaskPushNotificationConfig - if err := json.Unmarshal([]byte(row.Data), &cfg); err != nil { - return nil, fmt.Errorf("failed to deserialize push notification: %w", err) - } - return &cfg, nil + return parseVersionedPushConfig(row.Data, row.ProtocolVersion) } -func (c *postgresClient) ListPushNotifications(ctx context.Context, taskID string) ([]*protocol.TaskPushNotificationConfig, error) { +func (c *postgresClient) ListPushNotifications(ctx context.Context, taskID string) ([]*a2a.PushConfig, error) { rows, err := c.q.ListPushNotifications(ctx, taskID) if err != nil { return nil, fmt.Errorf("failed to list push notifications: %w", err) } - result := make([]*protocol.TaskPushNotificationConfig, 0, len(rows)) - for _, row := range rows { - var cfg protocol.TaskPushNotificationConfig - if err := json.Unmarshal([]byte(row.Data), &cfg); err != nil { - return nil, fmt.Errorf("failed to deserialize push notification: %w", err) + result := make([]*a2a.PushConfig, 0, len(rows)) + for i, row := range rows { + cfg, err := parseVersionedPushConfig(row.Data, row.ProtocolVersion) + if err != nil { + return nil, fmt.Errorf("failed to deserialize push notification row %d: %w", i, err) } - result = append(result, &cfg) + result = append(result, cfg) } return result, nil } @@ -737,14 +746,16 @@ func toEvent(r dbgen.Event) *dbpkg.Event { } } +//nolint:unused // Kept for parity with other row mappers and future raw task DB APIs. func toTask(r dbgen.Task) *dbpkg.Task { return &dbpkg.Task{ - ID: r.ID, - CreatedAt: derefTime(r.CreatedAt), - UpdatedAt: derefTime(r.UpdatedAt), - DeletedAt: r.DeletedAt, - Data: r.Data, - SessionID: derefStr(r.SessionID), + ID: r.ID, + CreatedAt: derefTime(r.CreatedAt), + UpdatedAt: derefTime(r.UpdatedAt), + DeletedAt: r.DeletedAt, + Data: r.Data, + ProtocolVersion: r.ProtocolVersion, + SessionID: derefStr(r.SessionID), } } @@ -866,6 +877,50 @@ func strPtrIfNotEmpty(s string) *string { return &s } +// parseVersionedTask parses a task from a string and a version, handles conversion from legacy to v1 format. +func parseVersionedTask(data string, version *string) (*a2a.Task, error) { + switch { + case version == nil || *version == "": + var legacyTask protocol.Task + if err := json.Unmarshal([]byte(data), &legacyTask); err != nil { + return nil, fmt.Errorf("failed to deserialize legacy task: %w", err) + } + task, err := trpcv0.ToV1Task(&legacyTask) + if err != nil { + return nil, fmt.Errorf("failed to convert legacy task to v1: %w", err) + } + return task, nil + case *version == trpcv0.ProtocolVersionV1: + var task a2a.Task + if err := json.Unmarshal([]byte(data), &task); err != nil { + return nil, fmt.Errorf("failed to deserialize v1 task: %w", err) + } + return &task, nil + default: + return nil, fmt.Errorf("unsupported task protocol_version %q", *version) + } +} + +// parseVersionedPushConfig parses a push notification config from a string and a version, handles conversion from legacy to v1 format. +func parseVersionedPushConfig(data string, version *string) (*a2a.PushConfig, error) { + switch { + case version == nil || *version == "": + var legacyCfg protocol.TaskPushNotificationConfig + if err := json.Unmarshal([]byte(data), &legacyCfg); err != nil { + return nil, fmt.Errorf("failed to deserialize legacy push notification: %w", err) + } + return trpcv0.ToV1PushConfig(&legacyCfg), nil + case *version == trpcv0.ProtocolVersionV1: + var cfg a2a.PushConfig + if err := json.Unmarshal([]byte(data), &cfg); err != nil { + return nil, fmt.Errorf("failed to deserialize v1 push notification: %w", err) + } + return &cfg, nil + default: + return nil, fmt.Errorf("unsupported push_notification protocol_version %q", *version) + } +} + func derefStr(s *string) string { if s != nil { return *s diff --git a/go/core/internal/database/client_test.go b/go/core/internal/database/client_test.go index b8493396ec..0768cddb97 100644 --- a/go/core/internal/database/client_test.go +++ b/go/core/internal/database/client_test.go @@ -7,13 +7,13 @@ import ( "testing" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/jackc/pgx/v5/pgxpool" dbpkg "github.com/kagent-dev/kagent/go/api/database" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/pgvector/pgvector-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) // TestConcurrentAgentUpserts verifies that concurrent StoreAgent calls @@ -325,7 +325,7 @@ func TestStoreTaskTouchesSessionActivity(t *testing.T) { require.NoError(t, err) time.Sleep(10 * time.Millisecond) - err = client.StoreTask(ctx, &protocol.Task{ + err = client.StoreTask(ctx, &a2a.Task{ ID: "task-1", ContextID: sessionID, }) diff --git a/go/core/internal/database/gen/models.go b/go/core/internal/database/gen/models.go index e05efa2ae8..20b8fb7a39 100644 --- a/go/core/internal/database/gen/models.go +++ b/go/core/internal/database/gen/models.go @@ -106,12 +106,13 @@ type Memory struct { } type PushNotification struct { - ID string - TaskID string - CreatedAt *time.Time - UpdatedAt *time.Time - DeletedAt *time.Time - Data string + ID string + TaskID string + CreatedAt *time.Time + UpdatedAt *time.Time + DeletedAt *time.Time + Data string + ProtocolVersion *string } type Session struct { @@ -126,12 +127,13 @@ type Session struct { } type Task struct { - ID string - CreatedAt *time.Time - UpdatedAt *time.Time - DeletedAt *time.Time - Data string - SessionID *string + ID string + CreatedAt *time.Time + UpdatedAt *time.Time + DeletedAt *time.Time + Data string + SessionID *string + ProtocolVersion *string } type Tool struct { diff --git a/go/core/internal/database/gen/push_notifications.sql.go b/go/core/internal/database/gen/push_notifications.sql.go index 73a7a00691..c502c58c13 100644 --- a/go/core/internal/database/gen/push_notifications.sql.go +++ b/go/core/internal/database/gen/push_notifications.sql.go @@ -10,7 +10,7 @@ import ( ) const getPushNotification = `-- name: GetPushNotification :one -SELECT id, task_id, created_at, updated_at, deleted_at, data FROM push_notification +SELECT id, task_id, created_at, updated_at, deleted_at, data, protocol_version FROM push_notification WHERE task_id = $1 AND id = $2 AND deleted_at IS NULL LIMIT 1 ` @@ -30,12 +30,13 @@ func (q *Queries) GetPushNotification(ctx context.Context, arg GetPushNotificati &i.UpdatedAt, &i.DeletedAt, &i.Data, + &i.ProtocolVersion, ) return i, err } const listPushNotifications = `-- name: ListPushNotifications :many -SELECT id, task_id, created_at, updated_at, deleted_at, data FROM push_notification +SELECT id, task_id, created_at, updated_at, deleted_at, data, protocol_version FROM push_notification WHERE task_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC ` @@ -56,6 +57,7 @@ func (q *Queries) ListPushNotifications(ctx context.Context, taskID string) ([]P &i.UpdatedAt, &i.DeletedAt, &i.Data, + &i.ProtocolVersion, ); err != nil { return nil, err } @@ -78,20 +80,27 @@ func (q *Queries) SoftDeletePushNotification(ctx context.Context, taskID string) } const upsertPushNotification = `-- name: UpsertPushNotification :exec -INSERT INTO push_notification (id, task_id, data, created_at, updated_at) -VALUES ($1, $2, $3, NOW(), NOW()) +INSERT INTO push_notification (id, task_id, data, protocol_version, created_at, updated_at) +VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET - data = EXCLUDED.data, - updated_at = NOW() + data = EXCLUDED.data, + protocol_version = EXCLUDED.protocol_version, + updated_at = NOW() ` type UpsertPushNotificationParams struct { - ID string - TaskID string - Data string + ID string + TaskID string + Data string + ProtocolVersion *string } func (q *Queries) UpsertPushNotification(ctx context.Context, arg UpsertPushNotificationParams) error { - _, err := q.db.Exec(ctx, upsertPushNotification, arg.ID, arg.TaskID, arg.Data) + _, err := q.db.Exec(ctx, upsertPushNotification, + arg.ID, + arg.TaskID, + arg.Data, + arg.ProtocolVersion, + ) return err } diff --git a/go/core/internal/database/gen/tasks.sql.go b/go/core/internal/database/gen/tasks.sql.go index e91be6daaf..be4e93d752 100644 --- a/go/core/internal/database/gen/tasks.sql.go +++ b/go/core/internal/database/gen/tasks.sql.go @@ -10,7 +10,7 @@ import ( ) const getTask = `-- name: GetTask :one -SELECT id, created_at, updated_at, deleted_at, data, session_id FROM task +SELECT id, created_at, updated_at, deleted_at, data, session_id, protocol_version FROM task WHERE id = $1 AND deleted_at IS NULL LIMIT 1 ` @@ -25,12 +25,13 @@ func (q *Queries) GetTask(ctx context.Context, id string) (Task, error) { &i.DeletedAt, &i.Data, &i.SessionID, + &i.ProtocolVersion, ) return i, err } const listTasksForSession = `-- name: ListTasksForSession :many -SELECT id, created_at, updated_at, deleted_at, data, session_id FROM task +SELECT id, created_at, updated_at, deleted_at, data, session_id, protocol_version FROM task WHERE session_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC ` @@ -51,6 +52,7 @@ func (q *Queries) ListTasksForSession(ctx context.Context, sessionID *string) ([ &i.DeletedAt, &i.Data, &i.SessionID, + &i.ProtocolVersion, ); err != nil { return nil, err } @@ -86,12 +88,13 @@ func (q *Queries) TaskExists(ctx context.Context, id string) (bool, error) { const upsertTask = `-- name: UpsertTask :exec WITH upserted_task AS ( -INSERT INTO task (id, data, session_id, created_at, updated_at) -VALUES ($1, $2, $3, NOW(), NOW()) +INSERT INTO task (id, data, session_id, protocol_version, created_at, updated_at) +VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET - data = EXCLUDED.data, - session_id = EXCLUDED.session_id, - updated_at = NOW() + data = EXCLUDED.data, + session_id = EXCLUDED.session_id, + protocol_version = EXCLUDED.protocol_version, + updated_at = NOW() RETURNING session_id ) UPDATE session @@ -103,12 +106,18 @@ WHERE upserted_task.session_id IS NOT NULL ` type UpsertTaskParams struct { - ID string - Data string - SessionID *string + ID string + Data string + SessionID *string + ProtocolVersion *string } func (q *Queries) UpsertTask(ctx context.Context, arg UpsertTaskParams) error { - _, err := q.db.Exec(ctx, upsertTask, arg.ID, arg.Data, arg.SessionID) + _, err := q.db.Exec(ctx, upsertTask, + arg.ID, + arg.Data, + arg.SessionID, + arg.ProtocolVersion, + ) return err } diff --git a/go/core/internal/database/queries/push_notifications.sql b/go/core/internal/database/queries/push_notifications.sql index ccc7553f69..d830497f97 100644 --- a/go/core/internal/database/queries/push_notifications.sql +++ b/go/core/internal/database/queries/push_notifications.sql @@ -9,11 +9,12 @@ WHERE task_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC; -- name: UpsertPushNotification :exec -INSERT INTO push_notification (id, task_id, data, created_at, updated_at) -VALUES ($1, $2, $3, NOW(), NOW()) +INSERT INTO push_notification (id, task_id, data, protocol_version, created_at, updated_at) +VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET - data = EXCLUDED.data, - updated_at = NOW(); + data = EXCLUDED.data, + protocol_version = EXCLUDED.protocol_version, + updated_at = NOW(); -- name: SoftDeletePushNotification :exec UPDATE push_notification SET deleted_at = NOW() diff --git a/go/core/internal/database/queries/tasks.sql b/go/core/internal/database/queries/tasks.sql index 66105c6f3a..d41fcdf928 100644 --- a/go/core/internal/database/queries/tasks.sql +++ b/go/core/internal/database/queries/tasks.sql @@ -15,12 +15,13 @@ ORDER BY created_at ASC; -- name: UpsertTask :exec WITH upserted_task AS ( -INSERT INTO task (id, data, session_id, created_at, updated_at) -VALUES ($1, $2, $3, NOW(), NOW()) +INSERT INTO task (id, data, session_id, protocol_version, created_at, updated_at) +VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET - data = EXCLUDED.data, - session_id = EXCLUDED.session_id, - updated_at = NOW() + data = EXCLUDED.data, + session_id = EXCLUDED.session_id, + protocol_version = EXCLUDED.protocol_version, + updated_at = NOW() RETURNING session_id ) UPDATE session diff --git a/go/core/internal/httpserver/handlers/sessions.go b/go/core/internal/httpserver/handlers/sessions.go index 1ff768077d..10e76a6327 100644 --- a/go/core/internal/httpserver/handlers/sessions.go +++ b/go/core/internal/httpserver/handlers/sessions.go @@ -6,13 +6,14 @@ import ( "strconv" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/database" api "github.com/kagent-dev/kagent/go/api/httpapi" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/httpserver/errors" "github.com/kagent-dev/kagent/go/core/internal/utils" + "github.com/kagent-dev/kagent/go/core/pkg/a2acompat/trpcv0" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) // SessionsHandler handles session-related requests @@ -119,7 +120,7 @@ func (h *SessionsHandler) HandleCreateSession(w ErrorResponseWriter, r *http.Req } log = log.WithValues("agentRef", *sessionRequest.AgentRef) - id := protocol.GenerateContextID() + id := a2a.NewContextID() if sessionRequest.ID != nil && *sessionRequest.ID != "" { id = *sessionRequest.ID } @@ -346,10 +347,34 @@ func (h *SessionsHandler) HandleListTasksForSession(w ErrorResponseWriter, r *ht w.RespondWithError(errors.NewInternalServerError("Failed to get session runs", err)) return } + wireVersion, err := utils.NegotiateA2AWireVersion(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", err)) + return + } log.Info("Successfully retrieved session tasks", "count", len(tasks)) - data := api.NewResponse(tasks, "Successfully retrieved session tasks", false) - RespondWithJSON(w, http.StatusOK, data) + + // TODO(cleanup): Remove legacy API conversion after legacy wire support is no longer supported. + switch wireVersion { + case utils.A2AWireVersionLegacy: + legacyTasks := make([]any, 0, len(tasks)) + for i := range tasks { + legacyTask, convErr := trpcv0.ToLegacyTask(tasks[i]) + if convErr != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to convert task", convErr)) + return + } + legacyTasks = append(legacyTasks, legacyTask) + } + data := api.NewResponse(legacyTasks, "Successfully retrieved session tasks", false) + RespondWithJSON(w, http.StatusOK, data) + case utils.A2AWireVersionV1: + data := api.NewResponse(tasks, "Successfully retrieved session tasks", false) + RespondWithJSON(w, http.StatusOK, data) + default: + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", fmt.Errorf("unknown negotiated wire version %q", wireVersion))) + } } func (h *SessionsHandler) HandleAddEventToSession(w ErrorResponseWriter, r *http.Request) { diff --git a/go/core/internal/httpserver/handlers/sessions_test.go b/go/core/internal/httpserver/handlers/sessions_test.go index 02ee3243d2..3e5627564d 100644 --- a/go/core/internal/httpserver/handlers/sessions_test.go +++ b/go/core/internal/httpserver/handlers/sessions_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,7 +25,6 @@ import ( "github.com/kagent-dev/kagent/go/core/internal/utils" "github.com/kagent-dev/kagent/go/core/pkg/auth" "github.com/kagent-dev/kmcp/api/v1alpha1" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) func setUser(req *http.Request, userID string) *http.Request { @@ -532,24 +532,25 @@ func TestSessionsHandler(t *testing.T) { agentID := "1" createTestSession(t, dbClient, sessionID, userID, agentID) - require.NoError(t, dbClient.StoreTask(context.Background(), &protocol.Task{ + require.NoError(t, dbClient.StoreTask(context.Background(), &a2a.Task{ ID: "task-1", ContextID: sessionID, })) - require.NoError(t, dbClient.StoreTask(context.Background(), &protocol.Task{ + require.NoError(t, dbClient.StoreTask(context.Background(), &a2a.Task{ ID: "task-2", ContextID: sessionID, })) req := httptest.NewRequest("GET", "/api/sessions/"+sessionID+"/tasks", nil) req = mux.SetURLVars(req, map[string]string{"session_id": sessionID}) + req.Header.Set("A2A-Version", "1.0") req = setUser(req, userID) handler.HandleListTasksForSession(responseRecorder, req) assert.Equal(t, http.StatusOK, responseRecorder.Code) - var response api.StandardResponse[[]*protocol.Task] + var response api.StandardResponse[[]*a2a.Task] err := json.Unmarshal(responseRecorder.Body.Bytes(), &response) require.NoError(t, err) assert.Len(t, response.Data, 2) diff --git a/go/core/internal/httpserver/handlers/tasks.go b/go/core/internal/httpserver/handlers/tasks.go index ccf0ee6e21..eefdc3707b 100644 --- a/go/core/internal/httpserver/handlers/tasks.go +++ b/go/core/internal/httpserver/handlers/tasks.go @@ -1,10 +1,14 @@ package handlers import ( + "fmt" "net/http" + a2a "github.com/a2aproject/a2a-go/v2/a2a" api "github.com/kagent-dev/kagent/go/api/httpapi" "github.com/kagent-dev/kagent/go/core/internal/httpserver/errors" + "github.com/kagent-dev/kagent/go/core/internal/utils" + "github.com/kagent-dev/kagent/go/core/pkg/a2acompat/trpcv0" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "trpc.group/trpc-go/trpc-a2a-go/protocol" ) @@ -34,22 +38,71 @@ func (h *TasksHandler) HandleGetTask(w ErrorResponseWriter, r *http.Request) { w.RespondWithError(errors.NewNotFoundError("Task not found", err)) return } + wireVersion, err := utils.NegotiateA2AWireVersion(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", err)) + return + } log.Info("Successfully retrieved task") - data := api.NewResponse(task, "Successfully retrieved task", false) - RespondWithJSON(w, http.StatusOK, data) + // TODO(cleanup): Remove legacy API conversion after legacy wire support is no longer supported. + // Currently this will return either legacy or v1 task depending on the wire version + var data any + switch wireVersion { + case utils.A2AWireVersionLegacy: + legacyTask, convErr := trpcv0.ToLegacyTask(task) + if convErr != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to convert task", convErr)) + return + } + data = legacyTask + case utils.A2AWireVersionV1: + data = task + default: + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", fmt.Errorf("unknown negotiated wire version %q", wireVersion))) + return + } + response := api.NewResponse(data, "Successfully retrieved task", false) + RespondWithJSON(w, http.StatusOK, response) } func (h *TasksHandler) HandleCreateTask(w ErrorResponseWriter, r *http.Request) { log := ctrllog.FromContext(r.Context()).WithName("tasks-handler").WithValues("operation", "create-task") - task := protocol.Task{} - if err := DecodeJSONBody(r, &task); err != nil { - w.RespondWithError(errors.NewBadRequestError("Invalid request body", err)) + wireVersion, err := utils.NegotiateA2AWireVersion(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", err)) + return + } + + task := a2a.Task{} + // TODO(cleanup): Remove legacy API conversion after legacy wire support is no longer supported. + switch wireVersion { + case utils.A2AWireVersionLegacy: + legacyTask := protocol.Task{} + if err := DecodeJSONBody(r, &legacyTask); err != nil { + w.RespondWithError(errors.NewBadRequestError("Invalid request body", err)) + return + } + converted, convErr := trpcv0.ToV1Task(&legacyTask) + if convErr != nil { + w.RespondWithError(errors.NewBadRequestError("Invalid legacy task payload", convErr)) + return + } + if converted != nil { + task = *converted + } + case utils.A2AWireVersionV1: + if err := DecodeJSONBody(r, &task); err != nil { + w.RespondWithError(errors.NewBadRequestError("Invalid request body", err)) + return + } + default: + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", fmt.Errorf("unknown negotiated wire version %q", wireVersion))) return } if task.ID == "" { - task.ID = protocol.GenerateTaskID() + task.ID = a2a.NewTaskID() } log = log.WithValues("task_id", task.ID) @@ -59,8 +112,23 @@ func (h *TasksHandler) HandleCreateTask(w ErrorResponseWriter, r *http.Request) } log.Info("Successfully created task") - data := api.NewResponse(task, "Successfully created task", false) - RespondWithJSON(w, http.StatusCreated, data) + var data any + switch wireVersion { + case utils.A2AWireVersionLegacy: + legacyTask, convErr := trpcv0.ToLegacyTask(&task) + if convErr != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to convert task", convErr)) + return + } + data = legacyTask + case utils.A2AWireVersionV1: + data = task + default: + w.RespondWithError(errors.NewBadRequestError("Unsupported A2A version", fmt.Errorf("unknown negotiated wire version %q", wireVersion))) + return + } + response := api.NewResponse(data, "Successfully created task", false) + RespondWithJSON(w, http.StatusCreated, response) } func (h *TasksHandler) HandleDeleteTask(w ErrorResponseWriter, r *http.Request) { diff --git a/go/core/internal/mcp/mcp_handler.go b/go/core/internal/mcp/mcp_handler.go index 804228cf0a..d65473f9b1 100644 --- a/go/core/internal/mcp/mcp_handler.go +++ b/go/core/internal/mcp/mcp_handler.go @@ -2,16 +2,19 @@ package mcp import ( "context" + "encoding/json" "fmt" "net/http" "strings" "sync" "time" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclientv2 "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" "github.com/google/jsonschema-go/jsonschema" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/a2a" - authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/core/internal/version" "github.com/kagent-dev/kagent/go/core/pkg/auth" "github.com/kagent-dev/kagent/go/core/pkg/env" @@ -19,8 +22,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - a2aclient "trpc.group/trpc-go/trpc-a2a-go/client" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) // MCPHandler handles MCP requests and bridges them to A2A endpoints @@ -211,38 +212,39 @@ func (h *MCPHandler) handleInvokeAgent(ctx context.Context, req *mcpsdk.CallTool agentRef := agentNS + "/" + agentName agentNns := types.NamespacedName{Namespace: agentNS, Name: agentName} - // Get context ID from client request (stateless mode) - // If not provided, contextIDPtr will be nil and a new conversation will start - var contextIDPtr *string - if input.ContextID != "" { - contextIDPtr = &input.ContextID - log.V(1).Info("Using context_id from client request", "context_id", input.ContextID) - } - // Get or create cached A2A client for this agent a2aURL := fmt.Sprintf("%s/%s/", h.a2aBaseURL, agentRef) - var a2aClient *a2aclient.A2AClient + var a2aClient *a2aclientv2.Client if cached, ok := h.a2aClients.Load(agentRef); ok { - if client, ok := cached.(*a2aclient.A2AClient); ok { + if client, ok := cached.(*a2aclientv2.Client); ok { a2aClient = client } } // Create new client if not cached if a2aClient == nil { - // Build A2A client options with authentication propagation - a2aOpts := []a2aclient.Option{ - a2aclient.WithTimeout(h.a2aTimeout), - a2aclient.WithHTTPReqHandler( - authimpl.A2ARequestHandler( - h.authenticator, - agentNns, - ), - ), + httpClient := &http.Client{Timeout: h.a2aTimeout} + resolver := agentcard.NewResolver(httpClient) + card, err := resolver.Resolve(ctx, a2aURL) + if err != nil { + log.Error(err, "Failed to resolve A2A card", "agent", agentRef) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Text: fmt.Sprintf("Failed to resolve A2A card: %v", err)}, + }, + IsError: true, + }, InvokeAgentOutput{}, nil } - - newClient, err := a2aclient.NewA2AClient(a2aURL, a2aOpts...) + newClient, err := a2aclientv2.NewFromCard( + ctx, + card, + a2aclientv2.WithJSONRPCTransport(httpClient), + a2aclientv2.WithCallInterceptors( + a2a.NewUpstreamAuthInterceptor(h.authenticator, agentNns), + a2a.NewStaticHeadersInterceptor(map[string]string{"A2A-Version": string(a2atype.Version)}), + ), + ) if err != nil { log.Error(err, "Failed to create A2A client", "agent", agentRef) return &mcpsdk.CallToolResult{ @@ -258,15 +260,9 @@ func (h *MCPHandler) handleInvokeAgent(ctx context.Context, req *mcpsdk.CallTool a2aClient = newClient } - // Send message via A2A - result, err := a2aClient.SendMessage(ctx, protocol.SendMessageParams{ - Message: protocol.Message{ - Kind: protocol.KindMessage, - Role: protocol.MessageRoleUser, - ContextID: contextIDPtr, - Parts: []protocol.Part{protocol.NewTextPart(input.Task)}, - }, - }) + message := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart(input.Task)) + message.ContextID = input.ContextID + result, err := a2aClient.SendMessage(ctx, &a2atype.SendMessageRequest{Message: message}) if err != nil { log.Error(err, "Failed to send A2A message", "agent", agentRef) return &mcpsdk.CallToolResult{ @@ -279,25 +275,22 @@ func (h *MCPHandler) handleInvokeAgent(ctx context.Context, req *mcpsdk.CallTool // Extract response text and context ID var responseText, newContextID string - switch a2aResult := result.Result.(type) { - case *protocol.Message: - responseText = a2a.ExtractText(*a2aResult) - if a2aResult.ContextID != nil { - newContextID = *a2aResult.ContextID - } - // Kagent A2A only returns Task type for now - case *protocol.Task: + switch a2aResult := result.(type) { + case *a2atype.Message: + responseText = a2a.ExtractText(a2aResult) + newContextID = a2aResult.ContextID + case *a2atype.Task: newContextID = a2aResult.ContextID if a2aResult.Status.Message != nil { - responseText = a2a.ExtractText(*a2aResult.Status.Message) + responseText = a2a.ExtractText(a2aResult.Status.Message) } for _, artifact := range a2aResult.Artifacts { - responseText += a2a.ExtractText(protocol.Message{Parts: artifact.Parts}) + responseText += a2a.ExtractText(&a2atype.Message{Parts: artifact.Parts}) } } if responseText == "" { - raw, err := result.MarshalJSON() + raw, err := json.Marshal(result) if err != nil { return &mcpsdk.CallToolResult{ Content: []mcpsdk.Content{ diff --git a/go/core/internal/utils/a2a_version.go b/go/core/internal/utils/a2a_version.go new file mode 100644 index 0000000000..042d701a32 --- /dev/null +++ b/go/core/internal/utils/a2a_version.go @@ -0,0 +1,31 @@ +package utils + +import ( + "fmt" + "net/http" + + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2acompat/a2av0" +) + +type A2AWireVersion string + +const ( + A2AWireVersionLegacy A2AWireVersion = "v0" + A2AWireVersionV1 A2AWireVersion = "v1" +) + +// NegotiateA2AWireVersion returns the A2A wire version requested by the client. +// Missing or explicit 0.3 headers use the legacy/current kagent A2A wire shape. +// TODO(cleanup): Revisit missing-header behavior once legacy wire clients are unsupported. +func NegotiateA2AWireVersion(r *http.Request) (A2AWireVersion, error) { + version := r.Header.Get(a2atype.SvcParamVersion) + switch version { + case "", string(a2av0.Version): + return A2AWireVersionLegacy, nil + case string(a2atype.Version): + return A2AWireVersionV1, nil + default: + return "", fmt.Errorf("unsupported A2A version %q", version) + } +} diff --git a/go/core/pkg/a2acompat/trpcv0/convert.go b/go/core/pkg/a2acompat/trpcv0/convert.go new file mode 100644 index 0000000000..dabc1ca0fe --- /dev/null +++ b/go/core/pkg/a2acompat/trpcv0/convert.go @@ -0,0 +1,561 @@ +package trpcv0 + +import ( + "encoding/json" + "fmt" + "maps" + "time" + + legacya2a "github.com/a2aproject/a2a-go/a2a" + a2av1 "github.com/a2aproject/a2a-go/v2/a2a" + a2av0 "github.com/a2aproject/a2a-go/v2/a2acompat/a2av0" + trpc "trpc.group/trpc-go/trpc-a2a-go/protocol" +) + +const ProtocolVersionV1 = string(a2av1.Version) + +// TaskJSONToV1JSON converts a persisted trpc-a2a-go task blob to official A2A v1 JSON. +func TaskJSONToV1JSON(data []byte) ([]byte, error) { + var task trpc.Task + if err := json.Unmarshal(data, &task); err != nil { + return nil, fmt.Errorf("unmarshal trpc task: %w", err) + } + v1, err := ToV1Task(&task) + if err != nil { + return nil, err + } + return json.Marshal(v1) +} + +// PushNotificationJSONToV1JSON converts a persisted trpc-a2a-go push config blob to official A2A v1 JSON. +func PushNotificationJSONToV1JSON(data []byte) ([]byte, error) { + var cfg trpc.TaskPushNotificationConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("unmarshal trpc push notification config: %w", err) + } + v1 := ToV1PushConfig(&cfg) + return json.Marshal(v1) +} + +func ToV1Task(task *trpc.Task) (*a2av1.Task, error) { + v0, err := ToOfficialV0Task(task) + if err != nil { + return nil, err + } + return a2av0.ToV1Task(v0) +} + +func ToV1PushConfig(cfg *trpc.TaskPushNotificationConfig) *a2av1.PushConfig { + return a2av0.ToV1PushConfig(ToOfficialV0PushConfig(cfg)) +} + +func ToLegacyTask(task *a2av1.Task) (*trpc.Task, error) { + if task == nil { + return nil, nil + } + message, err := toLegacyMessage(task.Status.Message) + if err != nil { + return nil, fmt.Errorf("convert status message: %w", err) + } + + result := &trpc.Task{ + ID: string(task.ID), + ContextID: task.ContextID, + Metadata: task.Metadata, + Status: trpc.TaskStatus{ + State: toLegacyTaskState(task.Status.State), + Message: message, + Timestamp: formatTimestamp(task.Status.Timestamp), + }, + } + + if len(task.History) > 0 { + result.History = make([]trpc.Message, 0, len(task.History)) + for i := range task.History { + msg, convErr := toLegacyMessage(task.History[i]) + if convErr != nil { + return nil, fmt.Errorf("convert history message %d: %w", i, convErr) + } + if msg != nil { + result.History = append(result.History, *msg) + } + } + } + if len(task.Artifacts) > 0 { + result.Artifacts = make([]trpc.Artifact, 0, len(task.Artifacts)) + for i := range task.Artifacts { + artifact, convErr := toLegacyArtifact(task.Artifacts[i]) + if convErr != nil { + return nil, fmt.Errorf("convert artifact %d: %w", i, convErr) + } + if artifact != nil { + result.Artifacts = append(result.Artifacts, *artifact) + } + } + } + return result, nil +} + +func ToLegacyPushConfig(cfg *a2av1.PushConfig) *trpc.TaskPushNotificationConfig { + if cfg == nil { + return nil + } + result := &trpc.TaskPushNotificationConfig{ + TaskID: string(cfg.TaskID), + PushNotificationConfig: trpc.PushNotificationConfig{ + ID: cfg.ID, + URL: cfg.URL, + Token: cfg.Token, + }, + } + if cfg.Auth != nil { + credentials := cfg.Auth.Credentials + schemes := []string{} + if cfg.Auth.Scheme != "" { + schemes = append(schemes, cfg.Auth.Scheme) + } + result.PushNotificationConfig.Authentication = &trpc.AuthenticationInfo{ + Credentials: &credentials, + Schemes: schemes, + } + } + return result +} + +func ToOfficialV0Task(task *trpc.Task) (*legacya2a.Task, error) { + if task == nil { + return nil, nil + } + status, err := ToOfficialV0TaskStatus(task.Status) + if err != nil { + return nil, err + } + result := &legacya2a.Task{ + ID: legacya2a.TaskID(task.ID), + ContextID: task.ContextID, + Metadata: task.Metadata, + Status: status, + } + if len(task.History) > 0 { + result.History = make([]*legacya2a.Message, len(task.History)) + for i := range task.History { + result.History[i], err = ToOfficialV0Message(&task.History[i]) + if err != nil { + return nil, fmt.Errorf("convert task history message %d: %w", i, err) + } + } + } + if len(task.Artifacts) > 0 { + result.Artifacts = make([]*legacya2a.Artifact, len(task.Artifacts)) + for i := range task.Artifacts { + result.Artifacts[i], err = ToOfficialV0Artifact(&task.Artifacts[i]) + if err != nil { + return nil, fmt.Errorf("convert task artifact %d: %w", i, err) + } + } + } + return result, nil +} + +func toLegacyMessage(message *a2av1.Message) (*trpc.Message, error) { + if message == nil { + return nil, nil + } + parts, err := toLegacyParts(message.Parts) + if err != nil { + return nil, err + } + + taskID := "" + if message.TaskID != "" { + taskID = string(message.TaskID) + } + contextID := message.ContextID + + result := &trpc.Message{ + Kind: trpc.KindMessage, + MessageID: message.ID, + ContextID: new(contextID), + Extensions: message.Extensions, + Metadata: message.Metadata, + Parts: parts, + ReferenceTaskIDs: toLegacyTaskIDs(message.ReferenceTasks), + Role: toLegacyMessageRole(message.Role), + TaskID: new(taskID), + } + return result, nil +} + +func toLegacyArtifact(artifact *a2av1.Artifact) (*trpc.Artifact, error) { + if artifact == nil { + return nil, nil + } + parts, err := toLegacyParts(artifact.Parts) + if err != nil { + return nil, err + } + id := string(artifact.ID) + name := artifact.Name + description := artifact.Description + + return &trpc.Artifact{ + ArtifactID: id, + Name: new(name), + Description: new(description), + Metadata: artifact.Metadata, + Extensions: artifact.Extensions, + Parts: parts, + }, nil +} + +func toLegacyParts(parts a2av1.ContentParts) ([]trpc.Part, error) { + if len(parts) == 0 { + return nil, nil + } + result := make([]trpc.Part, 0, len(parts)) + for i := range parts { + part, err := toLegacyPart(parts[i]) + if err != nil { + return nil, fmt.Errorf("convert part %d: %w", i, err) + } + result = append(result, part) + } + return result, nil +} + +func toLegacyPart(part *a2av1.Part) (trpc.Part, error) { + if part == nil { + return trpc.TextPart{Kind: trpc.KindText, Text: ""}, nil + } + if text := part.Text(); text != "" { + return trpc.TextPart{ + Kind: trpc.KindText, + Text: text, + Metadata: part.Metadata, + }, nil + } + if data := part.Data(); data != nil { + return trpc.DataPart{ + Kind: trpc.KindData, + Data: data, + Metadata: part.Metadata, + }, nil + } + if url := part.URL(); url != "" { + urlString := string(url) + fileName := part.Filename + mimeType := part.MediaType + return trpc.FilePart{ + Kind: trpc.KindFile, + File: &trpc.FileWithURI{ + Name: new(fileName), + MimeType: new(mimeType), + URI: urlString, + }, + Metadata: part.Metadata, + }, nil + } + raw := part.Raw() + if len(raw) > 0 { + fileName := part.Filename + mimeType := part.MediaType + return trpc.FilePart{ + Kind: trpc.KindFile, + File: &trpc.FileWithBytes{ + Name: new(fileName), + MimeType: new(mimeType), + Bytes: string(raw), + }, + Metadata: part.Metadata, + }, nil + } + return trpc.DataPart{ + Kind: trpc.KindData, + Data: map[string]any{}, + Metadata: part.Metadata, + }, nil +} + +func toLegacyTaskState(state a2av1.TaskState) trpc.TaskState { + switch state { + case a2av1.TaskStateSubmitted: + return trpc.TaskStateSubmitted + case a2av1.TaskStateWorking: + return trpc.TaskStateWorking + case a2av1.TaskStateInputRequired: + return trpc.TaskStateInputRequired + case a2av1.TaskStateCompleted: + return trpc.TaskStateCompleted + case a2av1.TaskStateCanceled: + return trpc.TaskStateCanceled + case a2av1.TaskStateFailed: + return trpc.TaskStateFailed + case a2av1.TaskStateRejected: + return trpc.TaskStateRejected + case a2av1.TaskStateAuthRequired: + return trpc.TaskStateAuthRequired + default: + return trpc.TaskStateUnknown + } +} + +func toLegacyMessageRole(role a2av1.MessageRole) trpc.MessageRole { + switch role { + case a2av1.MessageRoleAgent: + return trpc.MessageRoleAgent + case a2av1.MessageRoleUser: + return trpc.MessageRoleUser + default: + return trpc.MessageRoleAgent + } +} + +func toLegacyTaskIDs(ids []a2av1.TaskID) []string { + if len(ids) == 0 { + return nil + } + result := make([]string, len(ids)) + for i := range ids { + result[i] = string(ids[i]) + } + return result +} + +func formatTimestamp(timestamp *time.Time) string { + if timestamp == nil { + return "" + } + return timestamp.UTC().Format(time.RFC3339Nano) +} + +func ToOfficialV0TaskStatus(status trpc.TaskStatus) (legacya2a.TaskStatus, error) { + var msg *legacya2a.Message + var err error + if status.Message != nil { + msg, err = ToOfficialV0Message(status.Message) + if err != nil { + return legacya2a.TaskStatus{}, fmt.Errorf("convert task status message: %w", err) + } + } + timestamp, err := parseTimestamp(status.Timestamp) + if err != nil { + return legacya2a.TaskStatus{}, err + } + return legacya2a.TaskStatus{ + Message: msg, + State: ToOfficialV0TaskState(status.State), + Timestamp: timestamp, + }, nil +} + +func ToOfficialV0Message(message *trpc.Message) (*legacya2a.Message, error) { + if message == nil { + return nil, nil + } + parts, err := ToOfficialV0Parts(message.Parts) + if err != nil { + return nil, err + } + return &legacya2a.Message{ + ID: message.MessageID, + ContextID: derefString(message.ContextID), + Extensions: message.Extensions, + Metadata: message.Metadata, + Parts: parts, + ReferenceTasks: toOfficialV0TaskIDs(message.ReferenceTaskIDs), + Role: ToOfficialV0MessageRole(message.Role), + TaskID: legacya2a.TaskID(derefString(message.TaskID)), + }, nil +} + +func ToOfficialV0Artifact(artifact *trpc.Artifact) (*legacya2a.Artifact, error) { + if artifact == nil { + return nil, nil + } + parts, err := ToOfficialV0Parts(artifact.Parts) + if err != nil { + return nil, err + } + return &legacya2a.Artifact{ + ID: legacya2a.ArtifactID(artifact.ArtifactID), + Description: derefString(artifact.Description), + Extensions: artifact.Extensions, + Metadata: artifact.Metadata, + Name: derefString(artifact.Name), + Parts: parts, + }, nil +} + +func ToOfficialV0Parts(parts []trpc.Part) (legacya2a.ContentParts, error) { + if len(parts) == 0 { + return nil, nil + } + result := make(legacya2a.ContentParts, len(parts)) + for i, part := range parts { + converted, err := ToOfficialV0Part(part) + if err != nil { + return nil, fmt.Errorf("convert part %d: %w", i, err) + } + result[i] = converted + } + return result, nil +} + +func ToOfficialV0Part(part trpc.Part) (legacya2a.Part, error) { + switch p := part.(type) { + case nil: + return nil, nil + case trpc.TextPart: + return legacya2a.TextPart{Text: p.Text, Metadata: p.Metadata}, nil + case *trpc.TextPart: + return legacya2a.TextPart{Text: p.Text, Metadata: p.Metadata}, nil + case trpc.DataPart: + return toOfficialV0DataPart(p), nil + case *trpc.DataPart: + return toOfficialV0DataPart(*p), nil + case trpc.FilePart: + return ToOfficialV0FilePart(p) + case *trpc.FilePart: + return ToOfficialV0FilePart(*p) + default: + return nil, fmt.Errorf("unsupported trpc part type %T", part) + } +} + +func ToOfficialV0FilePart(part trpc.FilePart) (legacya2a.Part, error) { + switch file := part.File.(type) { + case nil: + return nil, fmt.Errorf("file part missing file payload") + case *trpc.FileWithBytes: + return officialV0FileBytes(*file, part.Metadata), nil + case *trpc.FileWithURI: + return officialV0FileURI(*file, part.Metadata), nil + default: + return nil, fmt.Errorf("unsupported trpc file payload type %T", part.File) + } +} + +func ToOfficialV0PushConfig(cfg *trpc.TaskPushNotificationConfig) *legacya2a.TaskPushConfig { + if cfg == nil { + return nil + } + pushConfig := legacya2a.PushConfig{ + ID: cfg.PushNotificationConfig.ID, + Token: cfg.PushNotificationConfig.Token, + URL: cfg.PushNotificationConfig.URL, + } + if cfg.PushNotificationConfig.Authentication != nil { + pushConfig.Auth = &legacya2a.PushAuthInfo{ + Credentials: derefString(cfg.PushNotificationConfig.Authentication.Credentials), + Schemes: cfg.PushNotificationConfig.Authentication.Schemes, + } + } + return &legacya2a.TaskPushConfig{ + Config: pushConfig, + TaskID: legacya2a.TaskID(cfg.TaskID), + } +} + +func ToOfficialV0TaskState(state trpc.TaskState) legacya2a.TaskState { + switch state { + case trpc.TaskStateSubmitted: + return legacya2a.TaskStateSubmitted + case trpc.TaskStateWorking: + return legacya2a.TaskStateWorking + case trpc.TaskStateInputRequired: + return legacya2a.TaskStateInputRequired + case trpc.TaskStateCompleted: + return legacya2a.TaskStateCompleted + case trpc.TaskStateCanceled: + return legacya2a.TaskStateCanceled + case trpc.TaskStateFailed: + return legacya2a.TaskStateFailed + case trpc.TaskStateRejected: + return legacya2a.TaskStateRejected + case trpc.TaskStateAuthRequired: + return legacya2a.TaskStateAuthRequired + case trpc.TaskStateUnknown: + return legacya2a.TaskStateUnknown + default: + return legacya2a.TaskStateUnspecified + } +} + +func ToOfficialV0MessageRole(role trpc.MessageRole) legacya2a.MessageRole { + switch role { + case trpc.MessageRoleAgent: + return legacya2a.MessageRoleAgent + case trpc.MessageRoleUser: + return legacya2a.MessageRoleUser + default: + return legacya2a.MessageRoleUnspecified + } +} + +func toOfficialV0DataPart(part trpc.DataPart) legacya2a.DataPart { + data, ok := part.Data.(map[string]any) + metadata := maps.Clone(part.Metadata) + if !ok { + data = map[string]any{"value": part.Data} + if metadata == nil { + metadata = map[string]any{} + } + metadata["data_part_compat"] = true + } + return legacya2a.DataPart{Data: data, Metadata: metadata} +} + +func officialV0FileBytes(file trpc.FileWithBytes, metadata map[string]any) legacya2a.FilePart { + return legacya2a.FilePart{ + File: legacya2a.FileBytes{ + FileMeta: legacya2a.FileMeta{ + MimeType: derefString(file.MimeType), + Name: derefString(file.Name), + }, + Bytes: file.Bytes, + }, + Metadata: metadata, + } +} + +func officialV0FileURI(file trpc.FileWithURI, metadata map[string]any) legacya2a.FilePart { + return legacya2a.FilePart{ + File: legacya2a.FileURI{ + FileMeta: legacya2a.FileMeta{ + MimeType: derefString(file.MimeType), + Name: derefString(file.Name), + }, + URI: file.URI, + }, + Metadata: metadata, + } +} + +func parseTimestamp(raw string) (*time.Time, error) { + if raw == "" { + return nil, nil + } + parsed, err := time.Parse(time.RFC3339Nano, raw) + if err != nil { + return nil, fmt.Errorf("parse task status timestamp %q: %w", raw, err) + } + return &parsed, nil +} + +func toOfficialV0TaskIDs(ids []string) []legacya2a.TaskID { + if len(ids) == 0 { + return nil + } + result := make([]legacya2a.TaskID, len(ids)) + for i, id := range ids { + result[i] = legacya2a.TaskID(id) + } + return result +} + +func derefString(value *string) string { + if value == nil { + return "" + } + return *value +} diff --git a/go/core/pkg/a2acompat/trpcv0/convert_test.go b/go/core/pkg/a2acompat/trpcv0/convert_test.go new file mode 100644 index 0000000000..44576d41c4 --- /dev/null +++ b/go/core/pkg/a2acompat/trpcv0/convert_test.go @@ -0,0 +1,385 @@ +package trpcv0 + +import ( + "encoding/json" + "testing" + "time" + + a2av1 "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/google/go-cmp/cmp" + trpc "trpc.group/trpc-go/trpc-a2a-go/protocol" +) + +func TestTaskJSONToV1JSON_ClusterTextTask(t *testing.T) { + task := mustConvertTaskJSONToV1(t, buildLegacyTextTaskFixture()) + assertForwardTextTaskFixture(t, task) +} + +func TestTaskJSONToV1JSON_ClusterDataTask(t *testing.T) { + task := mustConvertTaskJSONToV1(t, buildLegacyDataTaskFixture()) + assertForwardDataTaskFixture(t, task) +} + +func TestPushNotificationJSONToV1JSON(t *testing.T) { + cfg := mustConvertPushNotificationJSONToV1(t, buildLegacyPushConfigFixture()) + + want := a2av1.PushConfig{ + TaskID: "task-1", + ID: "cfg-1", + URL: "https://callback.example", + Token: "tok", + Auth: &a2av1.PushAuthInfo{ + Credentials: "cred", + Scheme: "Bearer", + }, + } + if diff := cmp.Diff(want, cfg); diff != "" { + t.Fatalf("unexpected push config (-want +got):\n%s", diff) + } +} + +func TestToLegacyTask_FromV1RichFixture(t *testing.T) { + v1Task := buildV1RichTaskFixture() + got := mustConvertToLegacyTask(t, v1Task) + assertBackwardTaskFixture(t, got) +} + +func TestToLegacyPushConfig_FromV1(t *testing.T) { + got := ToLegacyPushConfig(buildV1PushConfigFixture()) + if got == nil { + t.Fatal("expected non-nil config") + } + if got.TaskID != "task-v1-rich" || got.PushNotificationConfig.ID != "cfg-v1" || got.PushNotificationConfig.URL != "https://callback.example/v1" || got.PushNotificationConfig.Token != "token-v1" { + t.Fatalf("unexpected legacy push config: %+v", got) + } + if got.PushNotificationConfig.Authentication == nil { + t.Fatal("expected authentication") + } + if got.PushNotificationConfig.Authentication.Credentials == nil || *got.PushNotificationConfig.Authentication.Credentials != "secret" { + t.Fatalf("credentials = %v", got.PushNotificationConfig.Authentication.Credentials) + } + if len(got.PushNotificationConfig.Authentication.Schemes) != 1 || got.PushNotificationConfig.Authentication.Schemes[0] != "Bearer" { + t.Fatalf("schemes = %+v", got.PushNotificationConfig.Authentication.Schemes) + } +} + +func mustConvertTaskJSONToV1(t *testing.T, fixture trpc.Task) a2av1.Task { + t.Helper() + input, err := json.Marshal(fixture) + if err != nil { + t.Fatalf("marshal legacy task fixture: %v", err) + } + data, err := TaskJSONToV1JSON(input) + if err != nil { + t.Fatalf("TaskJSONToV1JSON() error = %v", err) + } + var task a2av1.Task + if err := json.Unmarshal(data, &task); err != nil { + t.Fatalf("unmarshal v1 task: %v\njson: %s", err, data) + } + return task +} + +func mustConvertPushNotificationJSONToV1(t *testing.T, fixture trpc.TaskPushNotificationConfig) a2av1.PushConfig { + t.Helper() + input, err := json.Marshal(fixture) + if err != nil { + t.Fatalf("marshal legacy push fixture: %v", err) + } + data, err := PushNotificationJSONToV1JSON(input) + if err != nil { + t.Fatalf("PushNotificationJSONToV1JSON() error = %v", err) + } + var cfg a2av1.PushConfig + if err := json.Unmarshal(data, &cfg); err != nil { + t.Fatalf("unmarshal v1 push config: %v", err) + } + return cfg +} + +func mustConvertToLegacyTask(t *testing.T, fixture *a2av1.Task) *trpc.Task { + t.Helper() + got, err := ToLegacyTask(fixture) + if err != nil { + t.Fatalf("ToLegacyTask() error = %v", err) + } + return got +} + +func assertForwardTextTaskFixture(t *testing.T, task a2av1.Task) { + t.Helper() + if task.ID != "019d49ab-6830-763c-9db6-1b6359228c4c" { + t.Fatalf("task ID = %q", task.ID) + } + if task.Status.State != a2av1.TaskStateCompleted { + t.Fatalf("task state = %q", task.Status.State) + } + if got := task.History[0].Role; got != a2av1.MessageRoleUser { + t.Fatalf("history role = %q", got) + } + if got := task.History[0].Parts[0].Text(); got != "hi" { + t.Fatalf("history text part = %q", got) + } + if got := task.Artifacts[0].Parts[0].Text(); got != "Hello! How can I assist you with Kubernetes today?" { + t.Fatalf("artifact text part = %q", got) + } +} + +func assertForwardDataTaskFixture(t *testing.T, task a2av1.Task) { + t.Helper() + if task.Status.State != a2av1.TaskStateInputRequired { + t.Fatalf("task state = %q", task.Status.State) + } + if task.Status.Message == nil { + t.Fatal("expected status message") + } + dataPart, ok := task.Status.Message.Parts[0].Data().(map[string]any) + if !ok { + t.Fatalf("status message part data type = %T", task.Status.Message.Parts[0].Data()) + } + if got := dataPart["name"]; got != "adk_request_confirmation" { + t.Fatalf("status message data name = %v", got) + } +} + +func assertBackwardTaskFixture(t *testing.T, got *trpc.Task) { + t.Helper() + if got.ID != "task-v1-rich" { + t.Fatalf("task ID = %q", got.ID) + } + if got.ContextID != "ctx-bridge" { + t.Fatalf("context ID = %q", got.ContextID) + } + if got.Status.State != trpc.TaskStateWorking { + t.Fatalf("status state = %q", got.Status.State) + } + wantTS := time.Date(2026, time.January, 2, 3, 4, 5, 123456000, time.UTC).Format(time.RFC3339Nano) + if got.Status.Timestamp != wantTS { + t.Fatalf("status timestamp = %q", got.Status.Timestamp) + } + if got.Status.Message == nil { + t.Fatal("expected status message") + } + if got.Status.Message.MessageID == "" { + t.Fatal("expected non-empty status message ID") + } + if got.Status.Message.TaskID == nil || *got.Status.Message.TaskID != "task-v1-rich" { + t.Fatalf("status message task ID = %v", got.Status.Message.TaskID) + } + + if len(got.History) != 1 { + t.Fatalf("history length = %d", len(got.History)) + } + if len(got.History[0].Parts) != 4 { + t.Fatalf("history parts length = %d", len(got.History[0].Parts)) + } + + textPart := mustTextPart(t, got.History[0].Parts[0]) + if textPart.Kind != trpc.KindText || textPart.Text != "hello" { + t.Fatalf("unexpected text part: %+v", textPart) + } + dataPart := mustDataPart(t, got.History[0].Parts[1]) + dataMap, ok := dataPart.Data.(map[string]any) + if !ok { + t.Fatalf("data part payload type = %T", dataPart.Data) + } + if dataMap["step"] != float64(1) { + t.Fatalf("data part step = %v", dataMap["step"]) + } + urlFilePart := mustFilePart(t, got.History[0].Parts[2]) + urlFile, ok := urlFilePart.File.(*trpc.FileWithURI) + if !ok { + t.Fatalf("expected FileWithURI, got %T", urlFilePart.File) + } + if urlFile.URI != "https://example.com/doc.md" { + t.Fatalf("file URI = %q", urlFile.URI) + } + rawFilePart := mustFilePart(t, got.History[0].Parts[3]) + rawFile, ok := rawFilePart.File.(*trpc.FileWithBytes) + if !ok { + t.Fatalf("expected FileWithBytes, got %T", rawFilePart.File) + } + if rawFile.Bytes != "RAW_BYTES" { + t.Fatalf("raw bytes = %q", rawFile.Bytes) + } + + if len(got.Artifacts) != 1 || len(got.Artifacts[0].Parts) != 1 { + t.Fatalf("unexpected artifacts: %+v", got.Artifacts) + } + if gotArtifactText := mustTextPart(t, got.Artifacts[0].Parts[0]).Text; gotArtifactText != "artifact-text" { + t.Fatalf("artifact text = %q", gotArtifactText) + } +} + +func mustTextPart(t *testing.T, part trpc.Part) trpc.TextPart { + t.Helper() + switch p := part.(type) { + case trpc.TextPart: + return p + case *trpc.TextPart: + return *p + default: + t.Fatalf("expected TextPart, got %T", part) + return trpc.TextPart{} + } +} + +func mustDataPart(t *testing.T, part trpc.Part) trpc.DataPart { + t.Helper() + switch p := part.(type) { + case trpc.DataPart: + return p + case *trpc.DataPart: + return *p + default: + t.Fatalf("expected DataPart, got %T", part) + return trpc.DataPart{} + } +} + +func mustFilePart(t *testing.T, part trpc.Part) trpc.FilePart { + t.Helper() + switch p := part.(type) { + case trpc.FilePart: + return p + case *trpc.FilePart: + return *p + default: + t.Fatalf("expected FilePart, got %T", part) + return trpc.FilePart{} + } +} + +func buildLegacyTextTaskFixture() trpc.Task { + return trpc.Task{ + ID: "019d49ab-6830-763c-9db6-1b6359228c4c", + Kind: trpc.KindTask, + ContextID: "ctx-text", + Status: trpc.TaskStatus{ + State: trpc.TaskStateCompleted, + Message: &trpc.Message{ + Kind: trpc.KindMessage, + MessageID: "msg-status-1", + Role: trpc.MessageRoleAgent, + Parts: []trpc.Part{ + trpc.TextPart{Kind: trpc.KindText, Text: "done"}, + }, + }, + }, + History: []trpc.Message{ + { + Kind: trpc.KindMessage, + MessageID: "msg-user-1", + Role: trpc.MessageRoleUser, + Parts: []trpc.Part{ + trpc.TextPart{Kind: trpc.KindText, Text: "hi"}, + }, + }, + }, + Artifacts: []trpc.Artifact{ + { + ArtifactID: "artifact-1", + Parts: []trpc.Part{ + trpc.TextPart{Kind: trpc.KindText, Text: "Hello! How can I assist you with Kubernetes today?"}, + }, + }, + }, + } +} + +func buildLegacyDataTaskFixture() trpc.Task { + return trpc.Task{ + ID: "task-data-1", + Kind: trpc.KindTask, + ContextID: "ctx-data", + Status: trpc.TaskStatus{ + State: trpc.TaskStateInputRequired, + Message: &trpc.Message{ + Kind: trpc.KindMessage, + MessageID: "msg-status-data-1", + Role: trpc.MessageRoleAgent, + Parts: []trpc.Part{ + trpc.DataPart{ + Kind: trpc.KindData, + Data: map[string]any{ + "name": "adk_request_confirmation", + }, + }, + }, + }, + }, + } +} + +func buildLegacyPushConfigFixture() trpc.TaskPushNotificationConfig { + cred := "cred" + return trpc.TaskPushNotificationConfig{ + TaskID: "task-1", + PushNotificationConfig: trpc.PushNotificationConfig{ + ID: "cfg-1", + URL: "https://callback.example", + Token: "tok", + Authentication: &trpc.AuthenticationInfo{ + Credentials: &cred, + Schemes: []string{"Bearer"}, + }, + }, + } +} + +func buildV1RichTaskFixture() *a2av1.Task { + ts := time.Date(2026, time.January, 2, 3, 4, 5, 123456000, time.UTC) + taskID := a2av1.TaskID("task-v1-rich") + fileURLPart := a2av1.NewFileURLPart(a2av1.URL("https://example.com/doc.md"), "text/markdown") + fileURLPart.Filename = "doc.md" + fileURLPart.Metadata = map[string]any{"source": "url"} + rawPart := a2av1.NewRawPart([]byte("RAW_BYTES")) + rawPart.Filename = "blob.bin" + rawPart.MediaType = "application/octet-stream" + rawPart.Metadata = map[string]any{"source": "raw"} + + statusMessage := a2av1.NewMessage(a2av1.MessageRoleAgent, a2av1.NewTextPart("working")) + statusMessage.TaskID = taskID + statusMessage.ContextID = "ctx-bridge" + + historyMessage := a2av1.NewMessage( + a2av1.MessageRoleUser, + a2av1.NewTextPart("hello"), + a2av1.NewDataPart(map[string]any{"step": float64(1)}), + fileURLPart, + rawPart, + ) + historyMessage.TaskID = taskID + historyMessage.ContextID = "ctx-bridge" + + return &a2av1.Task{ + ID: taskID, + ContextID: "ctx-bridge", + Metadata: map[string]any{"kagent": "true"}, + Status: a2av1.TaskStatus{ + State: a2av1.TaskStateWorking, + Timestamp: &ts, + Message: statusMessage, + }, + History: []*a2av1.Message{historyMessage}, + Artifacts: []*a2av1.Artifact{ + { + ID: "artifact-v1", + Parts: a2av1.ContentParts{a2av1.NewTextPart("artifact-text")}, + }, + }, + } +} + +func buildV1PushConfigFixture() *a2av1.PushConfig { + return &a2av1.PushConfig{ + TaskID: "task-v1-rich", + ID: "cfg-v1", + URL: "https://callback.example/v1", + Token: "token-v1", + Auth: &a2av1.PushAuthInfo{ + Credentials: "secret", + Scheme: "Bearer", + }, + } +} diff --git a/go/core/pkg/migrations/core/000005_a2a_protocol_version.down.sql b/go/core/pkg/migrations/core/000005_a2a_protocol_version.down.sql new file mode 100644 index 0000000000..ad866c8d26 --- /dev/null +++ b/go/core/pkg/migrations/core/000005_a2a_protocol_version.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE task DROP COLUMN IF EXISTS protocol_version; +ALTER TABLE push_notification DROP COLUMN IF EXISTS protocol_version; diff --git a/go/core/pkg/migrations/core/000005_a2a_protocol_version.up.sql b/go/core/pkg/migrations/core/000005_a2a_protocol_version.up.sql new file mode 100644 index 0000000000..123820c133 --- /dev/null +++ b/go/core/pkg/migrations/core/000005_a2a_protocol_version.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE task ADD COLUMN IF NOT EXISTS protocol_version TEXT; +ALTER TABLE push_notification ADD COLUMN IF NOT EXISTS protocol_version TEXT; diff --git a/go/core/pkg/translator/adk_api_translator_types.go b/go/core/pkg/translator/adk_api_translator_types.go index 07fd4268d5..df07f70546 100644 --- a/go/core/pkg/translator/adk_api_translator_types.go +++ b/go/core/pkg/translator/adk_api_translator_types.go @@ -3,17 +3,17 @@ package translator import ( "context" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/kagent/go/api/adk" "github.com/kagent-dev/kagent/go/api/v1alpha2" "sigs.k8s.io/controller-runtime/pkg/client" - "trpc.group/trpc-go/trpc-a2a-go/server" ) type AgentOutputs struct { Manifest []client.Object `json:"manifest,omitempty"` Config *adk.AgentConfig `json:"config,omitempty"` - AgentCard server.AgentCard `json:"agentCard"` + AgentCard a2a.AgentCard `json:"agentCard"` } type TranslatorPlugin interface { diff --git a/go/core/test/e2e/invoke_api_test.go b/go/core/test/e2e/invoke_api_test.go index 528fce2882..3f4452ce20 100644 --- a/go/core/test/e2e/invoke_api_test.go +++ b/go/core/test/e2e/invoke_api_test.go @@ -21,6 +21,9 @@ import ( "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + a2aclient "github.com/a2aproject/a2a-go/v2/a2aclient" + "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/a2a" "github.com/kagent-dev/kagent/go/core/internal/utils" @@ -29,8 +32,6 @@ import ( "github.com/kagent-dev/mockllm" "github.com/stretchr/testify/require" "sigs.k8s.io/controller-runtime/pkg/client/config" - a2aclient "trpc.group/trpc-go/trpc-a2a-go/client" - "trpc.group/trpc-go/trpc-a2a-go/protocol" ) //go:embed mocks @@ -231,28 +232,55 @@ func setupSandboxAgentWithOptions(t *testing.T, cli client.Client, modelConfigNa return agent } -// setupA2AClient creates an A2A client for the test agent -func setupA2AClient(t *testing.T, agent *v1alpha2.Agent) *a2aclient.A2AClient { - a2aURL := a2aURL(agent.Namespace, agent.Name, false) - a2aClient, err := a2aclient.NewA2AClient(a2aURL) +func newA2AClient(t *testing.T, baseURL string, httpClient *http.Client, headers map[string]string) *a2aclient.Client { + t.Helper() + if httpClient == nil { + httpClient = &http.Client{Timeout: 60 * time.Second} + } + if headers == nil { + headers = map[string]string{} + } + if _, ok := headers["A2A-Version"]; !ok { + headers["A2A-Version"] = string(a2atype.Version) + } + + resolver := agentcard.NewResolver(httpClient) + resolveOpts := make([]agentcard.ResolveOption, 0, len(headers)) + for k, v := range headers { + resolveOpts = append(resolveOpts, agentcard.WithRequestHeader(k, v)) + } + + card, err := resolver.Resolve(t.Context(), baseURL, resolveOpts...) require.NoError(t, err) + + a2aClient, err := a2aclient.NewFromCard( + t.Context(), + card, + a2aclient.WithJSONRPCTransport(httpClient), + a2aclient.WithCallInterceptors(a2a.NewStaticHeadersInterceptor(headers)), + ) + require.NoError(t, err) + return a2aClient } +// setupA2AClient creates an A2A client for the test agent +func setupA2AClient(t *testing.T, agent *v1alpha2.Agent) *a2aclient.Client { + return newA2AClient(t, a2aURL(agent.Namespace, agent.Name, false), nil, nil) +} + // setupSandboxA2AClient creates an A2A client for the test sandbox agent. -func setupSandboxA2AClient(t *testing.T, agent *v1alpha2.SandboxAgent) *a2aclient.A2AClient { - a2aClient, err := a2aclient.NewA2AClient(a2aURL(agent.Namespace, agent.Name, true)) - require.NoError(t, err) - return a2aClient +func setupSandboxA2AClient(t *testing.T, agent *v1alpha2.SandboxAgent) *a2aclient.Client { + return newA2AClient(t, a2aURL(agent.Namespace, agent.Name, true), nil, nil) } // extractTextFromArtifacts extracts all text content from task artifacts -func extractTextFromArtifacts(taskResult *protocol.Task) string { +func extractTextFromArtifacts(taskResult *a2atype.Task) string { var text strings.Builder for _, artifact := range taskResult.Artifacts { for _, part := range artifact.Parts { - if textPart, ok := part.(*protocol.TextPart); ok { - text.WriteString(textPart.Text) + if part != nil { + text.WriteString(part.Text()) } } } @@ -269,22 +297,18 @@ var defaultRetry = wait.Backoff{ // runSyncTest runs a synchronous message test // useArtifacts: if true, check artifacts; if false or nil, check history; // contextID: optional context ID to maintain conversation context -func runSyncTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string, useArtifacts *bool, contextID ...string) *protocol.Task { +func runSyncTest(t *testing.T, a2aClient *a2aclient.Client, userMessage, expectedText string, useArtifacts *bool, contextID ...string) *a2atype.Task { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - msg := protocol.Message{ - Kind: protocol.KindMessage, - Role: protocol.MessageRoleUser, - Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, - } + msg := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart(userMessage)) // If contextID is provided, set it to maintain conversation context if len(contextID) > 0 && contextID[0] != "" { - msg.ContextID = &contextID[0] + msg.ContextID = contextID[0] } - var result *protocol.MessageResult + var result a2atype.SendMessageResult err := retry.OnError(defaultRetry, func(err error) bool { return err != nil }, func() error { @@ -294,13 +318,13 @@ func runSyncTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expe ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() t.Logf("%s trying to send message", time.Now().Format(time.RFC3339)) - result, retryErr = a2aClient.SendMessage(ctx, protocol.SendMessageParams{Message: msg}) + result, retryErr = a2aClient.SendMessage(ctx, &a2atype.SendMessageRequest{Message: msg}) t.Logf("%s finished trying sending message. success = %v", time.Now().Format(time.RFC3339), retryErr == nil) return retryErr }) require.NoError(t, err) - taskResult, ok := result.Result.(*protocol.Task) + taskResult, ok := result.(*a2atype.Task) require.True(t, ok) // Extract text based on useArtifacts flag @@ -322,22 +346,18 @@ func runSyncTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expe // runStreamingTest runs a streaming message test // If contextID is provided, it will be included in the message to maintain conversation context // Checks the full JSON output to support both artifacts and history from different agent types -func runStreamingTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string, contextID ...string) { - msg := protocol.Message{ - Kind: protocol.KindMessage, - Role: protocol.MessageRoleUser, - Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, - } +func runStreamingTest(t *testing.T, a2aClient *a2aclient.Client, userMessage, expectedText string, contextID ...string) { + msg := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart(userMessage)) // If contextID is provided, set it to maintain conversation context if len(contextID) > 0 && contextID[0] != "" { - msg.ContextID = &contextID[0] + msg.ContextID = contextID[0] } // Retry the entire stream-connect-read-check cycle. // The most common failure mode is: stream connects but yields zero events // (agent not ready, stream closes early), so we need to retry the whole operation. - var lastJSON string + var lastText string err := retry.OnError(defaultRetry, func(err error) bool { return err != nil }, func() error { @@ -345,35 +365,51 @@ func runStreamingTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, defer cancel() t.Logf("%s trying to open stream", time.Now().Format(time.RFC3339)) - stream, streamErr := a2aClient.StreamMessage(ctx, protocol.SendMessageParams{Message: msg}) - if streamErr != nil { - t.Logf("%s stream connection failed: %v", time.Now().Format(time.RFC3339), streamErr) - return streamErr - } - - resultList := []protocol.StreamingMessageEvent{} - for event := range stream { - if _, ok := event.Result.(*protocol.TaskStatusUpdateEvent); !ok { + stream := a2aClient.SendStreamingMessage(ctx, &a2atype.SendMessageRequest{Message: msg}) + texts := make([]string, 0) + eventCount := 0 + for event, streamErr := range stream { + if streamErr != nil { + t.Logf("%s stream read failed: %v", time.Now().Format(time.RFC3339), streamErr) + return streamErr + } + eventCount++ + if event == nil { continue } - resultList = append(resultList, event) + texts = append(texts, extractTextFromEvent(event)) } + lastText = strings.Join(texts, "\n") - jsn, marshalErr := json.Marshal(resultList) - if marshalErr != nil { - return marshalErr + if !strings.Contains(lastText, expectedText) { + t.Logf("%s stream completed but expected text %q not found in response (got %d events)", time.Now().Format(time.RFC3339), expectedText, eventCount) + return fmt.Errorf("expected text %q not found in streaming response (%d events)", expectedText, eventCount) } - lastJSON = string(jsn) - if !strings.Contains(lastJSON, expectedText) { - t.Logf("%s stream completed but expected text %q not found in response (got %d events)", time.Now().Format(time.RFC3339), expectedText, len(resultList)) - return fmt.Errorf("expected text %q not found in streaming response (%d events)", expectedText, len(resultList)) - } - - t.Logf("%s stream completed successfully with %d events", time.Now().Format(time.RFC3339), len(resultList)) + t.Logf("%s stream completed successfully with %d events", time.Now().Format(time.RFC3339), eventCount) return nil }) - require.NoError(t, err, lastJSON) + require.NoError(t, err, lastText) +} + +func extractTextFromEvent(event a2atype.Event) string { + switch e := event.(type) { + case *a2atype.TaskStatusUpdateEvent: + return a2a.ExtractText(e.Status.Message) + case *a2atype.TaskArtifactUpdateEvent: + return a2a.ExtractText(&a2atype.Message{Parts: e.Artifact.Parts}) + case *a2atype.Message: + return a2a.ExtractText(e) + case *a2atype.Task: + text := strings.Builder{} + if e.Status.Message != nil { + text.WriteString(a2a.ExtractText(e.Status.Message)) + } + text.WriteString(extractTextFromArtifacts(e)) + return text.String() + default: + return "" + } } func a2aURL(namespace, name string, sandbox bool) string { @@ -660,7 +696,7 @@ func TestE2EInvokeSandboxAgent(t *testing.T) { agent := setupSandboxAgentWithOptions(t, cli, modelCfg.Name, tools, AgentOptions{Stream: true}) a2aClient := setupSandboxA2AClient(t, agent) - var taskResult *protocol.Task + var taskResult *a2atype.Task t.Run("sync_invocation", func(t *testing.T) { taskResult = runSyncTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane", nil) @@ -674,8 +710,7 @@ func TestE2EInvokeSandboxAgent(t *testing.T) { func TestE2EInvokeExternalAgent(t *testing.T) { // Setup A2A client for external agent a2aURL := a2aUrl("kagent", "kebab-agent") - a2aClient, err := a2aclient.NewA2AClient(a2aURL) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, nil, nil) // Run tests t.Run("sync_invocation", func(t *testing.T) { @@ -688,8 +723,7 @@ func TestE2EInvokeExternalAgent(t *testing.T) { t.Run("invocation with different user", func(t *testing.T) { // Setup A2A client with authentication - authClient, err := a2aclient.NewA2AClient(a2aURL, a2aclient.WithAPIKeyAuth("user@example.com", "x-user-id")) - require.NoError(t, err) + authClient := newA2AClient(t, a2aURL, nil, map[string]string{"x-user-id": "user@example.com"}) runSyncTest(t, authClient, "What can you do?", "kebab for user@example.com", nil) }) @@ -887,8 +921,7 @@ func TestE2EInvokeOpenAIAgent(t *testing.T) { // Setup A2A client - use the agent's actual name a2aURL := a2aUrl("kagent", "basic-openai-test-agent") - a2aClient, err := a2aclient.NewA2AClient(a2aURL) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, nil, nil) useArtifacts := true t.Run("sync_invocation_calculator", func(t *testing.T) { @@ -950,8 +983,7 @@ func TestE2EInvokeLangGraphAgent(t *testing.T) { // Setup A2A client a2aURL := a2aUrl(agent.Namespace, agent.Name) - a2aClient, err := a2aclient.NewA2AClient(a2aURL) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, nil, nil) t.Run("sync_invocation", func(t *testing.T) { runSyncTest(t, a2aClient, "make me a kebab", "kebab is ready", nil) @@ -1024,8 +1056,7 @@ func TestE2EInvokeCrewAIAgent(t *testing.T) { // Setup A2A client a2aURL := a2aUrl(agent.Namespace, agent.Name) - a2aClient, err := a2aclient.NewA2AClient(a2aURL) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, nil, nil) t.Run("two_turn_conversation", func(t *testing.T) { // First turn: Generate initial poem @@ -1116,10 +1147,7 @@ func runE2EInvokeSTSIntegration(t *testing.T, runtimeName string, runtimeOverrid } a2aURL := a2aUrl(agent.Namespace, agent.Name) - a2aClient, err := a2aclient.NewA2AClient(a2aURL, - a2aclient.WithTimeout(60*time.Second), - a2aclient.WithHTTPClient(httpClient)) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, httpClient, nil) t.Run(runtimeName+"/sts_exchange_sync_invocation", func(t *testing.T) { runSyncTest(t, a2aClient, "add 3 and 5", "8", nil) @@ -1324,10 +1352,7 @@ func TestE2EInvokePassthroughAgent(t *testing.T) { }, } a2aURL := a2aUrl(agent.Namespace, agent.Name) - a2aClient, err := a2aclient.NewA2AClient(a2aURL, - a2aclient.WithTimeout(60*time.Second), - a2aclient.WithHTTPClient(httpClient)) - require.NoError(t, err) + a2aClient := newA2AClient(t, a2aURL, httpClient, nil) // The mock server will only match if it receives the exact // Authorization header "Bearer passthrough-test-token-12345". @@ -1390,7 +1415,7 @@ func runMemoryAgentTest(t *testing.T, extraOpts AgentOptions) { agent := setupAgentWithOptions(t, cli, llmModelCfg.Name, nil, opts) a2aClient := setupA2AClient(t, agent) - var saveResult *protocol.Task + var saveResult *a2atype.Task t.Run("save_memory", func(t *testing.T) { saveResult = runSyncTest(t, a2aClient, "Remember that I prefer dark mode and Go over Python", diff --git a/go/go.mod b/go/go.mod index 1b498ee8d8..4706abeaaf 100644 --- a/go/go.mod +++ b/go/go.mod @@ -8,6 +8,7 @@ require ( // adk dependencies github.com/a2aproject/a2a-go v0.3.15 + github.com/a2aproject/a2a-go/v2 v2.3.1 github.com/abiosoft/ishell/v2 v2.0.2 github.com/anthropics/anthropic-sdk-go v1.43.0 github.com/aws/aws-sdk-go-v2/config v1.32.17 @@ -18,7 +19,6 @@ require ( github.com/fatih/color v1.19.0 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 - github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-migrate/migrate/v4 v4.19.1 // api dependencies github.com/google/uuid v1.6.0 @@ -63,7 +63,9 @@ require ( require ( github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.6 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang/protobuf v1.5.4 + github.com/google/go-cmp v0.7.0 github.com/google/jsonschema-go v0.4.3 github.com/jackc/pgx/v5 v5.9.2 github.com/ollama/ollama v0.24.0 @@ -85,7 +87,7 @@ require ( cel.dev/expr v0.25.1 // indirect charm.land/lipgloss/v2 v2.0.3 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect @@ -236,11 +238,10 @@ require ( github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/safehtml v0.1.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect @@ -391,7 +392,7 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -417,9 +418,9 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/api v0.272.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/api v0.279.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect honnef.co/go/tools v0.7.0 // indirect diff --git a/go/go.sum b/go/go.sum index 7392185179..d9ebf2cc37 100644 --- a/go/go.sum +++ b/go/go.sum @@ -8,8 +8,8 @@ charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -64,6 +64,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+NiI= github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I= +github.com/a2aproject/a2a-go/v2 v2.3.1 h1:QWMdOX2UsJ8BJmjs952eo1FRyGsOVl0gFCKeM76AgGE= +github.com/a2aproject/a2a-go/v2 v2.3.1/go.mod h1:mkZr8y2bUgAVQsjs/5fHK7xrRlAHDybMEyxWh2tKRC8= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+SNo= @@ -424,10 +426,10 @@ github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8 github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= -github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -869,8 +871,8 @@ go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -1027,14 +1029,16 @@ 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.2.0 h1:MfQD1/GqPfIsFNBcozNykkjdqNIdCrPH/SNqKPZF/yM= google.golang.org/adk v1.2.0/go.mod h1:6QY5jQI7awU4WYtJqvyIkJQheCvqsGWweU6BX63USEc= -google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= -google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +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.57.0 h1:qTyG2ynz5dQy2jF4CvZdLHHVslhR0heMue+zM1a4GNM= google.golang.org/genai v1.57.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo= +google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= diff --git a/ui/src/app/actions/servers.ts b/ui/src/app/actions/servers.ts index 7441187b0c..99183b09fb 100644 --- a/ui/src/app/actions/servers.ts +++ b/ui/src/app/actions/servers.ts @@ -18,7 +18,7 @@ export async function getServers(): Promise> return { message: "MCP servers fetched successfully", - data: response.data, + data: response.data ?? [], }; } catch (error) { return createErrorResponse(error, "Error getting MCP servers");