From 18514f25f6b905869eba41b9854e3f0b2dbdc112 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Mon, 31 Aug 2026 13:34:21 -0700 Subject: [PATCH 1/7] feat(cli): add AgentTemplate lifecycle commands Signed-off-by: Cody Hartsook --- go/api/client/agenttemplate.go | 53 ++++ go/api/client/agenttemplategrpc_test.go | 97 +++++++ go/api/client/clientset.go | 2 + .../cli/internal/commands/agent_template.go | 245 +++++++++++++++++- .../internal/commands/agent_template_test.go | 117 +++++++++ go/core/cli/root.go | 6 + go/core/cli/root_test.go | 23 +- 7 files changed, 540 insertions(+), 3 deletions(-) create mode 100644 go/api/client/agenttemplate.go create mode 100644 go/api/client/agenttemplategrpc_test.go diff --git a/go/api/client/agenttemplate.go b/go/api/client/agenttemplate.go new file mode 100644 index 000000000..4c7476bd1 --- /dev/null +++ b/go/api/client/agenttemplate.go @@ -0,0 +1,53 @@ +package client + +import ( + "context" + + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" +) + +// AgentTemplateClient provides supported AgentTemplate operations. +type AgentTemplateClient struct { + client *BaseClient +} + +// NewAgentTemplateClient creates an AgentTemplate client over the shared gRPC connection. +func NewAgentTemplateClient(client *BaseClient) *AgentTemplateClient { + return &AgentTemplateClient{client: client} +} + +func (c *AgentTemplateClient) CreateAgentTemplate(ctx context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { + client, callContext, cancel, err := c.client.agentTemplateCall(ctx) + if err != nil { + return nil, err + } + defer cancel() + return client.CreateAgentTemplate(callContext, request) +} + +func (c *AgentTemplateClient) UpdateAgentTemplate(ctx context.Context, request *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) { + client, callContext, cancel, err := c.client.agentTemplateCall(ctx) + if err != nil { + return nil, err + } + defer cancel() + return client.UpdateAgentTemplate(callContext, request) +} + +func (c *AgentTemplateClient) DeleteAgentTemplate(ctx context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { + client, callContext, cancel, err := c.client.agentTemplateCall(ctx) + if err != nil { + return nil, err + } + defer cancel() + return client.DeleteAgentTemplate(callContext, request) +} + +func (c *BaseClient) agentTemplateCall(ctx context.Context) (apiv1alpha1.AgentTemplateServiceClient, context.Context, context.CancelFunc, error) { + connection, err := c.grpcConnection() + if err != nil { + return nil, nil, nil, err + } + callContext, cancel := c.grpcCallContext(ctx) + return apiv1alpha1.NewAgentTemplateServiceClient(connection), callContext, cancel, nil +} diff --git a/go/api/client/agenttemplategrpc_test.go b/go/api/client/agenttemplategrpc_test.go new file mode 100644 index 000000000..c1ed07d2a --- /dev/null +++ b/go/api/client/agenttemplategrpc_test.go @@ -0,0 +1,97 @@ +package client + +import ( + "context" + "net" + "sync" + "testing" + "time" + + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" +) + +type recordingAgentTemplateService struct { + apiv1alpha1.UnimplementedAgentTemplateServiceServer + + mu sync.Mutex + observations []callObservation + create *apiv1alpha1.CreateAgentTemplateRequest + update *apiv1alpha1.UpdateAgentTemplateRequest + delete *apiv1alpha1.DeleteAgentTemplateRequest +} + +func (s *recordingAgentTemplateService) CreateAgentTemplate(ctx context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { + s.observe(ctx) + s.mu.Lock() + defer s.mu.Unlock() + s.create = request + return &apiv1alpha1.CreateAgentTemplateResponse{}, nil +} + +func (s *recordingAgentTemplateService) UpdateAgentTemplate(ctx context.Context, request *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) { + s.observe(ctx) + s.mu.Lock() + defer s.mu.Unlock() + s.update = request + return &apiv1alpha1.UpdateAgentTemplateResponse{}, nil +} + +func (s *recordingAgentTemplateService) DeleteAgentTemplate(ctx context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { + s.observe(ctx) + s.mu.Lock() + defer s.mu.Unlock() + s.delete = request + return &apiv1alpha1.DeleteAgentTemplateResponse{}, nil +} + +func (s *recordingAgentTemplateService) observe(ctx context.Context) { + values, _ := metadata.FromIncomingContext(ctx) + _, hasDeadline := ctx.Deadline() + s.mu.Lock() + defer s.mu.Unlock() + s.observations = append(s.observations, callObservation{userID: first(values.Get(userIDHeader)), hasDeadline: hasDeadline}) +} + +func TestAgentTemplateClientUsesGeneratedGRPC(t *testing.T) { + listener := bufconn.Listen(1024 * 1024) + service := &recordingAgentTemplateService{} + server := grpc.NewServer() + apiv1alpha1.RegisterAgentTemplateServiceServer(server, service) + go func() { _ = server.Serve(listener) }() + t.Cleanup(func() { + server.Stop() + _ = listener.Close() + }) + + clientSet := New( + "http://rest-must-not-be-used.invalid", + WithUserID("caller"), + WithGRPCTarget("passthrough:///bufnet"), + WithGRPCTimeout(5*time.Second), + WithGRPCDialOptions(grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return listener.Dial() + })), + ) + t.Cleanup(func() { require.NoError(t, clientSet.Close()) }) + + ref := &apiv1alpha1.ResourceReference{Namespace: "team-a", Name: "researcher"} + _, err := clientSet.AgentTemplate.CreateAgentTemplate(t.Context(), &apiv1alpha1.CreateAgentTemplateRequest{Ref: ref}) + require.NoError(t, err) + _, err = clientSet.AgentTemplate.UpdateAgentTemplate(t.Context(), &apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref}) + require.NoError(t, err) + _, err = clientSet.AgentTemplate.DeleteAgentTemplate(t.Context(), &apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}) + require.NoError(t, err) + + service.mu.Lock() + defer service.mu.Unlock() + assert.True(t, proto.Equal(ref, service.create.GetRef())) + assert.True(t, proto.Equal(ref, service.update.GetRef())) + assert.True(t, proto.Equal(ref, service.delete.GetRef())) + assert.Equal(t, []callObservation{{userID: "caller", hasDeadline: true}, {userID: "caller", hasDeadline: true}, {userID: "caller", hasDeadline: true}}, service.observations) +} diff --git a/go/api/client/clientset.go b/go/api/client/clientset.go index 6c68b9971..041aab980 100644 --- a/go/api/client/clientset.go +++ b/go/api/client/clientset.go @@ -16,6 +16,7 @@ type ClientSet struct { Namespace Namespace Feedback Feedback AgentInstance *AgentInstanceClient + AgentTemplate *AgentTemplateClient A2A *A2AClient } @@ -37,6 +38,7 @@ func New(baseURL string, options ...ClientOption) *ClientSet { Namespace: NewNamespaceClient(baseClient), Feedback: NewFeedbackClient(baseClient), AgentInstance: NewAgentInstanceClient(baseClient), + AgentTemplate: NewAgentTemplateClient(baseClient), A2A: NewA2AClient(baseClient), } } diff --git a/go/core/cli/internal/commands/agent_template.go b/go/core/cli/internal/commands/agent_template.go index 2d38521b0..66cecad37 100644 --- a/go/core/cli/internal/commands/agent_template.go +++ b/go/core/cli/internal/commands/agent_template.go @@ -5,21 +5,31 @@ import ( "errors" "fmt" "io" + "os" "strings" "time" "github.com/jedib0t/go-pretty/v6/table" typedapiv1alpha3 "github.com/kagent-dev/kagent/go/api/clientset/versioned/typed/api/v1alpha3" + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/structuredobject" apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" commonk8s "github.com/kagent-dev/kagent/go/core/cli/internal/common/k8s" "github.com/kagent-dev/kagent/go/core/cli/internal/connection" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/spf13/cobra" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" ) -const agentTemplateMaxPageSize = 100 +const ( + agentTemplateKind = "AgentTemplate" + agentTemplateMaxPageSize = 100 +) // AgentTemplateGetCfg configures AgentTemplate get and list operations. type AgentTemplateGetCfg struct { @@ -30,6 +40,161 @@ type AgentTemplateGetCfg struct { PageToken string } +// AgentTemplateManifestCfg configures an AgentTemplate manifest operation. +type AgentTemplateManifestCfg struct { + OutputFormat string + File string +} + +// AgentTemplateDeleteCfg configures AgentTemplate deletion. +type AgentTemplateDeleteCfg struct { + OutputFormat string + Name string +} + +type agentTemplateLifecycleClient interface { + CreateAgentTemplate(context.Context, *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) + UpdateAgentTemplate(context.Context, *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) + DeleteAgentTemplate(context.Context, *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) +} + +type agentTemplateManifestOperation func(context.Context, agentTemplateLifecycleClient, *apiv1alpha1.ResourceReference, *apiv1alpha1.StructuredObject, clioutput.Format, io.Writer) error + +func runAgentTemplateManifest( + ctx context.Context, + options connection.Options, + cfg *AgentTemplateManifestCfg, + out io.Writer, + operation agentTemplateManifestOperation, +) (err error) { + format, err := clioutput.Parse(cfg.OutputFormat) + if err != nil { + return err + } + ref, resource, err := readAgentTemplateManifest(cfg.File, options.Namespace) + if err != nil { + return err + } + session, err := connection.Open(ctx, options) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, session.Close()) + }() + return operation(ctx, session.Client.AgentTemplate, ref, resource, format, out) +} + +func readAgentTemplateManifest(filename, namespace string) (*apiv1alpha1.ResourceReference, *apiv1alpha1.StructuredObject, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, nil, fmt.Errorf("read AgentTemplate manifest %q: %w", filename, err) + } + manifest := &apiv1alpha3.AgentTemplate{} + if err := yaml.UnmarshalStrict(data, manifest); err != nil { + return nil, nil, fmt.Errorf("parse AgentTemplate manifest %q: %w", filename, err) + } + if manifest.APIVersion != apiv1alpha3.GroupVersion.String() || manifest.Kind != agentTemplateKind { + return nil, nil, fmt.Errorf("AgentTemplate manifest %q must have apiVersion %q and kind %q", filename, apiv1alpha3.GroupVersion.String(), agentTemplateKind) + } + if manifest.Name == "" { + return nil, nil, fmt.Errorf("AgentTemplate manifest %q must have metadata.name", filename) + } + if manifest.Namespace != "" && manifest.Namespace != namespace { + return nil, nil, fmt.Errorf("AgentTemplate manifest namespace %q does not match --namespace %q", manifest.Namespace, namespace) + } + resource, err := structuredobject.FromGo(manifest, apiv1alpha3.GroupVersion.String(), agentTemplateKind, 0) + if err != nil { + return nil, nil, fmt.Errorf("encode AgentTemplate manifest %q: %w", filename, err) + } + return &apiv1alpha1.ResourceReference{Namespace: namespace, Name: manifest.Name}, resource, nil +} + +func createAgentTemplate( + ctx context.Context, + client agentTemplateLifecycleClient, + ref *apiv1alpha1.ResourceReference, + resource *apiv1alpha1.StructuredObject, + format clioutput.Format, + out io.Writer, +) error { + response, err := client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{Ref: ref, Resource: resource}) + if err != nil { + return fmt.Errorf("create AgentTemplate: %w", err) + } + return writeAgentTemplateResult(out, format, response, response.GetAgentTemplate()) +} + +func updateAgentTemplate( + ctx context.Context, + client agentTemplateLifecycleClient, + ref *apiv1alpha1.ResourceReference, + resource *apiv1alpha1.StructuredObject, + format clioutput.Format, + out io.Writer, +) error { + response, err := client.UpdateAgentTemplate(ctx, &apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}) + if err != nil { + return fmt.Errorf("update AgentTemplate: %w", err) + } + return writeAgentTemplateResult(out, format, response, response.GetAgentTemplate()) +} + +func applyAgentTemplate( + ctx context.Context, + client agentTemplateLifecycleClient, + ref *apiv1alpha1.ResourceReference, + resource *apiv1alpha1.StructuredObject, + format clioutput.Format, + out io.Writer, +) error { + created, err := client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{Ref: ref, Resource: resource}) + if status.Code(err) != codes.AlreadyExists { + if err != nil { + return fmt.Errorf("apply AgentTemplate: %w", err) + } + return writeAgentTemplateResult(out, format, created, created.GetAgentTemplate()) + } + updated, err := client.UpdateAgentTemplate(ctx, &apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}) + if err != nil { + return fmt.Errorf("apply AgentTemplate: %w", err) + } + return writeAgentTemplateResult(out, format, updated, updated.GetAgentTemplate()) +} + +func deleteAgentTemplate(ctx context.Context, client agentTemplateLifecycleClient, namespace string, cfg *AgentTemplateDeleteCfg, format clioutput.Format, out io.Writer) error { + response, err := client.DeleteAgentTemplate(ctx, &apiv1alpha1.DeleteAgentTemplateRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: namespace, Name: cfg.Name}, + }) + if err != nil { + return fmt.Errorf("delete AgentTemplate: %w", err) + } + if format == clioutput.FormatJSON { + return clioutput.WriteProto(out, response) + } + tw := table.NewWriter() + tw.AppendHeader(table.Row{"NAME", "STATUS"}) + tw.AppendRow(table.Row{cfg.Name, "DELETED"}) + if _, err := fmt.Fprintln(out, tw.Render()); err != nil { + return fmt.Errorf("write AgentTemplate output: %w", err) + } + return nil +} + +func writeAgentTemplateResult(w io.Writer, format clioutput.Format, response proto.Message, result *apiv1alpha1.AgentTemplate) error { + if result == nil { + return errors.New("AgentTemplate operation returned no AgentTemplate") + } + if format == clioutput.FormatJSON { + return clioutput.WriteProto(w, response) + } + template := &apiv1alpha3.AgentTemplate{} + if err := structuredobject.ToGo(result.GetResource(), agentTemplateKind, template, 0); err != nil { + return fmt.Errorf("decode AgentTemplate result: %w", err) + } + return writeAgentTemplatesTable(w, []apiv1alpha3.AgentTemplate{*template}, false, "") +} + // runGetAgentTemplate gets one AgentTemplate or lists AgentTemplates through Kubernetes. func runGetAgentTemplate(ctx context.Context, cfg *AgentTemplateGetCfg, out io.Writer) error { format, err := clioutput.Parse(cfg.OutputFormat) @@ -154,3 +319,81 @@ func NewGetAgentTemplateCmd() *cobra.Command { cmd.Flags().StringVar(&cfg.PageToken, "page-token", "", "Token returned by the previous page") return cmd } + +// NewCreateAgentTemplateCmd constructs the AgentTemplate create command. +func NewCreateAgentTemplateCmd() *cobra.Command { + return newAgentTemplateManifestCmd("agent-template", "Create an AgentTemplate", createAgentTemplate) +} + +// NewUpdateAgentTemplateCmd constructs the AgentTemplate update command. +func NewUpdateAgentTemplateCmd() *cobra.Command { + return newAgentTemplateManifestCmd("agent-template", "Update an AgentTemplate", updateAgentTemplate) +} + +// NewApplyAgentTemplateCmd constructs the AgentTemplate apply command. +func NewApplyAgentTemplateCmd() *cobra.Command { + return newAgentTemplateManifestCmd("apply -f FILE", "Create or update an AgentTemplate", applyAgentTemplate) +} + +func newAgentTemplateManifestCmd(use, short string, operation agentTemplateManifestOperation) *cobra.Command { + cfg := &AgentTemplateManifestCfg{} + cmd := &cobra.Command{ + Use: use, + Short: short, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + cfg.OutputFormat = format + return runAgentTemplateManifest(cmd.Context(), options, cfg, cmd.OutOrStdout(), operation) + }, + } + cmd.Flags().StringVarP(&cfg.File, "file", "f", "", "Path to AgentTemplate manifest") + _ = cmd.MarkFlagRequired("file") + return cmd +} + +// NewDeleteAgentTemplateCmd constructs the AgentTemplate delete command. +func NewDeleteAgentTemplateCmd() *cobra.Command { + cfg := &AgentTemplateDeleteCfg{} + cmd := &cobra.Command{ + Use: "agent-template NAME", + Short: "Delete an AgentTemplate", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + options, err := connection.OptionsFromCommand(cmd) + if err != nil { + return err + } + format, err := clioutput.FromCommand(cmd) + if err != nil { + return err + } + cfg.OutputFormat = format + cfg.Name = args[0] + return runDeleteAgentTemplate(cmd.Context(), options, cfg, cmd.OutOrStdout()) + }, + } + return cmd +} + +func runDeleteAgentTemplate(ctx context.Context, options connection.Options, cfg *AgentTemplateDeleteCfg, out io.Writer) (err error) { + format, err := clioutput.Parse(cfg.OutputFormat) + if err != nil { + return err + } + session, err := connection.Open(ctx, options) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, session.Close()) + }() + return deleteAgentTemplate(ctx, session.Client.AgentTemplate, session.Namespace, cfg, format, out) +} diff --git a/go/core/cli/internal/commands/agent_template_test.go b/go/core/cli/internal/commands/agent_template_test.go index 95a4aec65..11ba8ee96 100644 --- a/go/core/cli/internal/commands/agent_template_test.go +++ b/go/core/cli/internal/commands/agent_template_test.go @@ -4,13 +4,20 @@ import ( "bytes" "context" "encoding/json" + "os" + "path/filepath" "testing" clientfake "github.com/kagent-dev/kagent/go/api/clientset/versioned/fake" + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/structuredobject" apiv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" clioutput "github.com/kagent-dev/kagent/go/core/cli/internal/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8stesting "k8s.io/client-go/testing" @@ -115,3 +122,113 @@ func templateWithReadyCondition(name, harness string, status metav1.ConditionSta }}}, } } + +func TestReadAgentTemplateManifest(t *testing.T) { + manifestPath := filepath.Join(t.TempDir(), "template.yaml") + require.NoError(t, os.WriteFile(manifestPath, []byte(` +apiVersion: kagent.dev/v1alpha3 +kind: AgentTemplate +metadata: + name: researcher +spec: + modelConfig: + name: default +`), 0o600)) + + ref, resource, err := readAgentTemplateManifest(manifestPath, "team-a") + require.NoError(t, err) + assert.Equal(t, &apiv1alpha1.ResourceReference{Namespace: "team-a", Name: "researcher"}, ref) + decoded := &apiv1alpha3.AgentTemplate{} + require.NoError(t, structuredobject.ToGo(resource, agentTemplateKind, decoded, 0)) + assert.Equal(t, "default", decoded.Spec.ModelConfig.Name) + + require.NoError(t, os.WriteFile(manifestPath, []byte(` +apiVersion: kagent.dev/v1alpha3 +kind: AgentTemplate +metadata: + name: researcher + namespace: other +spec: + modelConfig: + name: default +`), 0o600)) + _, _, err = readAgentTemplateManifest(manifestPath, "team-a") + require.ErrorContains(t, err, `does not match --namespace`) +} + +func TestAgentTemplateLifecycleCommands(t *testing.T) { + template := testAgentTemplateMessage(t) + ref := template.GetRef() + resource := template.GetResource() + + t.Run("create", func(t *testing.T) { + client := &recordingAgentTemplateClient{template: template} + var output bytes.Buffer + require.NoError(t, createAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatTable, &output)) + assert.True(t, proto.Equal(&apiv1alpha1.CreateAgentTemplateRequest{Ref: ref, Resource: resource}, client.createRequest)) + assert.Contains(t, output.String(), "researcher") + }) + + t.Run("update", func(t *testing.T) { + client := &recordingAgentTemplateClient{template: template} + require.NoError(t, updateAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatJSON, &bytes.Buffer{})) + assert.True(t, proto.Equal(&apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}, client.updateRequest)) + }) + + t.Run("apply updates existing", func(t *testing.T) { + client := &recordingAgentTemplateClient{template: template, createErr: status.Error(codes.AlreadyExists, "exists")} + require.NoError(t, applyAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatTable, &bytes.Buffer{})) + assert.NotNil(t, client.createRequest) + assert.True(t, proto.Equal(&apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}, client.updateRequest)) + }) + + t.Run("delete", func(t *testing.T) { + client := &recordingAgentTemplateClient{} + var output bytes.Buffer + require.NoError(t, deleteAgentTemplate(t.Context(), client, "team-a", &AgentTemplateDeleteCfg{Name: "researcher"}, clioutput.FormatTable, &output)) + assert.True(t, proto.Equal(&apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}, client.deleteRequest)) + assert.Contains(t, output.String(), "researcher") + assert.Contains(t, output.String(), "DELETED") + }) +} + +func testAgentTemplateMessage(t *testing.T) *apiv1alpha1.AgentTemplate { + t.Helper() + template := &apiv1alpha3.AgentTemplate{ + TypeMeta: metav1.TypeMeta{APIVersion: apiv1alpha3.GroupVersion.String(), Kind: agentTemplateKind}, + ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "researcher"}, + Spec: apiv1alpha3.AgentTemplateSpec{ModelConfig: apiv1alpha3.AgentTemplateLocalReference{Name: "default"}}, + } + resource, err := structuredobject.FromGo(template, apiv1alpha3.GroupVersion.String(), agentTemplateKind, 0) + require.NoError(t, err) + return &apiv1alpha1.AgentTemplate{ + Ref: &apiv1alpha1.ResourceReference{Namespace: template.Namespace, Name: template.Name}, + Resource: resource, + } +} + +type recordingAgentTemplateClient struct { + template *apiv1alpha1.AgentTemplate + createErr error + createRequest *apiv1alpha1.CreateAgentTemplateRequest + updateRequest *apiv1alpha1.UpdateAgentTemplateRequest + deleteRequest *apiv1alpha1.DeleteAgentTemplateRequest +} + +func (c *recordingAgentTemplateClient) CreateAgentTemplate(_ context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { + c.createRequest = request + if c.createErr != nil { + return nil, c.createErr + } + return &apiv1alpha1.CreateAgentTemplateResponse{AgentTemplate: c.template}, nil +} + +func (c *recordingAgentTemplateClient) UpdateAgentTemplate(_ context.Context, request *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) { + c.updateRequest = request + return &apiv1alpha1.UpdateAgentTemplateResponse{AgentTemplate: c.template}, nil +} + +func (c *recordingAgentTemplateClient) DeleteAgentTemplate(_ context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { + c.deleteRequest = request + return &apiv1alpha1.DeleteAgentTemplateResponse{}, nil +} diff --git a/go/core/cli/root.go b/go/core/cli/root.go index 8803cdfe1..643473b8f 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -28,17 +28,23 @@ func Root() *cobra.Command { getCmd := newResourceGroupCmd("get", "Get a kagent resource") createCmd := newResourceGroupCmd("create", "Create a kagent resource") + updateCmd := newResourceGroupCmd("update", "Update a kagent resource") deleteCmd := newResourceGroupCmd("delete", "Delete a kagent resource") getCmd.AddCommand(agentinstancecli.NewGetCmd()) getCmd.AddCommand(commands.NewGetAgentTemplateCmd()) createCmd.AddCommand(agentinstancecli.NewCreateCmd()) + createCmd.AddCommand(commands.NewCreateAgentTemplateCmd()) + updateCmd.AddCommand(commands.NewUpdateAgentTemplateCmd()) deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd()) + deleteCmd.AddCommand(commands.NewDeleteAgentTemplateCmd()) rootCmd.AddCommand( getCmd, createCmd, + updateCmd, deleteCmd, + commands.NewApplyAgentTemplateCmd(), agentinstancecli.NewInvokeCmd(), commands.NewInstallCmd(), commands.NewUninstallCmd(), diff --git a/go/core/cli/root_test.go b/go/core/cli/root_test.go index d2012bc69..1a6f5c58d 100644 --- a/go/core/cli/root_test.go +++ b/go/core/cli/root_test.go @@ -124,6 +124,20 @@ func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { require.NoError(t, err) assert.Equal(t, "agent-instance ID", deleteInstanceCmd.Use) + for _, command := range [][]string{{"create", "agent-template"}, {"update", "agent-template"}} { + cmd, _, err := rootCmd.Find(command) + require.NoError(t, err) + assert.Equal(t, "agent-template", cmd.Use) + assert.NotNil(t, cmd.Flags().Lookup("file")) + } + applyCmd, _, err := rootCmd.Find([]string{"apply"}) + require.NoError(t, err) + assert.Equal(t, "apply -f FILE", applyCmd.Use) + assert.NotNil(t, applyCmd.Flags().Lookup("file")) + deleteTemplateCmd, _, err := rootCmd.Find([]string{"delete", "agent-template"}) + require.NoError(t, err) + assert.Equal(t, "agent-template NAME", deleteTemplateCmd.Use) + for _, command := range []string{"suspend", "resume"} { _, _, err := rootCmd.Find([]string{command, "agent-instance"}) assert.Error(t, err, "%s must not be exposed by the CLI", command) @@ -173,7 +187,11 @@ func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { "get agent-instance": {"get", "agent-instance"}, "get agent-template": {"get", "agent-template"}, "create agent-instance": {"create", "agent-instance", "--harness", "kagent", "--agent-template", "example"}, + "create agent-template": {"create", "agent-template", "--file", "template.yaml"}, + "apply agent-template": {"apply", "--file", "template.yaml"}, + "update agent-template": {"update", "agent-template", "--file", "template.yaml"}, "delete agent-instance": {"delete", "agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab"}, + "delete agent-template": {"delete", "agent-template", "example"}, "invoke": {"invoke", "--agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab", "--task", "hello"}, } { t.Run(name, func(t *testing.T) { @@ -193,8 +211,9 @@ func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { func TestRootResourceGroupsNameAvailableTypes(t *testing.T) { for name, want := range map[string]string{ "get": "agent-instance, agent-template", - "create": "agent-instance", - "delete": "agent-instance", + "create": "agent-instance, agent-template", + "update": "agent-template", + "delete": "agent-instance, agent-template", } { t.Run(name, func(t *testing.T) { rootCmd := cli.Root() From 2db8ff8b340b23dbd4026b4f2514d02ff76611f0 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Mon, 31 Aug 2026 13:48:12 -0700 Subject: [PATCH 2/7] test(cli): cover AgentTemplate lifecycle Signed-off-by: Cody Hartsook --- .../test/e2e/cli_catalog_lifecycle_test.go | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/go/core/test/e2e/cli_catalog_lifecycle_test.go b/go/core/test/e2e/cli_catalog_lifecycle_test.go index 8fbd74c8f..ed020f74d 100644 --- a/go/core/test/e2e/cli_catalog_lifecycle_test.go +++ b/go/core/test/e2e/cli_catalog_lifecycle_test.go @@ -4,14 +4,19 @@ import ( "bytes" "context" "encoding/json" + "fmt" "os" "os/exec" + "path/filepath" "strings" "testing" "github.com/google/uuid" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + v1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" "google.golang.org/protobuf/encoding/protojson" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/clientcmd" ) @@ -90,6 +95,95 @@ func TestE2ECLIAgentTemplateCatalogAndInstanceLifecycle(t *testing.T) { } } +func TestE2ECLIAgentTemplateLifecycle(t *testing.T) { + target := interactionTarget(t) + kube := interactionKubeClient(t) + model := createInteractionModel(t, kube, startInteractionMock(t), nil) + templateName := "cli-lifecycle-" + uuid.NewString() + manifestPath := filepath.Join(t.TempDir(), "agent-template.yaml") + binary := kagentCLI(t) + baseArgs := []string{ + "--grpc-url", target, + "--grpc-tls=false", + "--namespace", "kagent", + "--user-id", "e2e", + } + run := func(args ...string) string { + return runKagentCLI(t, t.Context(), binary, append(append([]string{}, baseArgs...), args...)...) + } + writeManifest := func(systemPrompt string) { + t.Helper() + manifest := fmt.Sprintf(`apiVersion: kagent.dev/v1alpha3 +kind: AgentTemplate +metadata: + name: %s + labels: + kagent.dev/e2e-runtime: kagent +spec: + modelConfig: + name: %s + systemPrompt: %q +`, templateName, model.Name, systemPrompt) + if err := os.WriteFile(manifestPath, []byte(manifest), 0o600); err != nil { + t.Fatalf("write AgentTemplate manifest: %v", err) + } + } + assertPrompt := func(want string) { + t.Helper() + template := &v1alpha3.AgentTemplate{} + if err := kube.Get(t.Context(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template); err != nil { + t.Fatalf("get AgentTemplate %q: %v", templateName, err) + } + if template.Spec.SystemPrompt != want { + t.Fatalf("AgentTemplate systemPrompt = %q, want %q", template.Spec.SystemPrompt, want) + } + } + deleteTemplate := func() { + t.Helper() + output := run("--output-format", "json", "delete", "agent-template", templateName) + if !json.Valid([]byte(output)) { + t.Fatalf("delete AgentTemplate stdout = %q, want JSON", output) + } + template := &v1alpha3.AgentTemplate{} + err := kube.Get(t.Context(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template) + if !apierrors.IsNotFound(err) { + t.Fatalf("get deleted AgentTemplate error = %v, want NotFound", err) + } + } + t.Cleanup(func() { + template := &v1alpha3.AgentTemplate{} + if err := kube.Get(context.Background(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template); err == nil { + if err := kube.Delete(context.Background(), template); err != nil { + t.Errorf("cleanup AgentTemplate %q: %v", templateName, err) + } + } + }) + + writeManifest("created through the CLI") + created := run("--output-format", "json", "create", "agent-template", "-f", manifestPath) + if !json.Valid([]byte(created)) || !strings.Contains(created, `"name":"`+templateName+`"`) { + t.Fatalf("create AgentTemplate stdout = %q, want JSON for %q", created, templateName) + } + assertPrompt("created through the CLI") + + writeManifest("updated through the CLI") + run("update", "agent-template", "-f", manifestPath) + assertPrompt("updated through the CLI") + + writeManifest("applied through the CLI") + run("apply", "-f", manifestPath) + assertPrompt("applied through the CLI") + + deleteTemplate() + + applied := run("--output-format", "json", "apply", "-f", manifestPath) + if !json.Valid([]byte(applied)) || !strings.Contains(applied, `"name":"`+templateName+`"`) { + t.Fatalf("apply AgentTemplate stdout = %q, want JSON for %q", applied, templateName) + } + assertPrompt("applied through the CLI") + deleteTemplate() +} + func TestE2ECLIAgentInstanceDiscoveryAndInvoke(t *testing.T) { t.Parallel() target := interactionTarget(t) From e2b84208120029cd8e47f6aacc90ae0484b82cd5 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Mon, 31 Aug 2026 14:40:17 -0700 Subject: [PATCH 3/7] fix(api): persist AgentTemplate labels on update Signed-off-by: Cody Hartsook --- go/core/internal/grpcserver/agenttemplate.go | 2 ++ .../grpcserver/agenttemplate_harness_test.go | 15 ++++++++-- .../test/e2e/cli_catalog_lifecycle_test.go | 29 ++++++++++--------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/go/core/internal/grpcserver/agenttemplate.go b/go/core/internal/grpcserver/agenttemplate.go index ce7cb8e69..807508edd 100644 --- a/go/core/internal/grpcserver/agenttemplate.go +++ b/go/core/internal/grpcserver/agenttemplate.go @@ -2,6 +2,7 @@ package grpcserver import ( "context" + "maps" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/api/structuredobject" @@ -86,6 +87,7 @@ func (s *agentTemplateServer) UpdateAgentTemplate(ctx context.Context, request * return nil, err } existing.Spec = *incoming.Spec.DeepCopy() + existing.Labels = maps.Clone(incoming.Labels) result, err := s.service.SaveUpdate(ctx, existing) if err != nil { return nil, err diff --git a/go/core/internal/grpcserver/agenttemplate_harness_test.go b/go/core/internal/grpcserver/agenttemplate_harness_test.go index 8c9c13f3f..f7acc2fce 100644 --- a/go/core/internal/grpcserver/agenttemplate_harness_test.go +++ b/go/core/internal/grpcserver/agenttemplate_harness_test.go @@ -119,9 +119,11 @@ func TestAgentTemplateServiceGeneratedClient(t *testing.T) { ctx := metadata.NewOutgoingContext(t.Context(), metadata.Pairs("x-user-id", "template-user")) ref := &apiv1alpha1.ResourceReference{Namespace: "team", Name: "a-created"} + createdTemplate := testAgentTemplate("team", "a-created", "gpt") + createdTemplate.Labels = map[string]string{"runtime": "kagent"} created, err := client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{ Ref: ref, - Resource: structured(t, testAgentTemplate("team", "a-created", "gpt"), agentTemplateKind), + Resource: structured(t, createdTemplate, agentTemplateKind), }) if err != nil { t.Fatalf("CreateAgentTemplate() error = %v", err) @@ -147,9 +149,11 @@ func TestAgentTemplateServiceGeneratedClient(t *testing.T) { t.Fatalf("GetAgentTemplate() description = %q", got.GetAgentTemplate().GetDescription()) } + updatedTemplate := testAgentTemplate("team", "a-created", "claude") + updatedTemplate.Labels = map[string]string{"runtime": "codex"} updated, err := client.UpdateAgentTemplate(ctx, &apiv1alpha1.UpdateAgentTemplateRequest{ Ref: ref, - Resource: structured(t, testAgentTemplate("team", "a-created", "claude"), agentTemplateKind), + Resource: structured(t, updatedTemplate, agentTemplateKind), }) if err != nil { t.Fatalf("UpdateAgentTemplate() error = %v", err) @@ -157,6 +161,13 @@ func TestAgentTemplateServiceGeneratedClient(t *testing.T) { if updated.GetAgentTemplate().GetModelConfigRef().GetName() != "claude" { t.Fatalf("UpdateAgentTemplate() modelConfigRef = %+v", updated.GetAgentTemplate().GetModelConfigRef()) } + updatedResource := &v1alpha3.AgentTemplate{} + if err := structuredobject.ToGo(updated.GetAgentTemplate().GetResource(), agentTemplateKind, updatedResource, DefaultMaxMessageSize); err != nil { + t.Fatalf("decode UpdateAgentTemplate() resource: %v", err) + } + if got := updatedResource.Labels["runtime"]; got != "codex" { + t.Fatalf("UpdateAgentTemplate() label runtime = %q, want codex", got) + } listed, err := client.ListAgentTemplates(ctx, &apiv1alpha1.ListAgentTemplatesRequest{Namespace: "team"}) if err != nil { diff --git a/go/core/test/e2e/cli_catalog_lifecycle_test.go b/go/core/test/e2e/cli_catalog_lifecycle_test.go index ed020f74d..7ea966855 100644 --- a/go/core/test/e2e/cli_catalog_lifecycle_test.go +++ b/go/core/test/e2e/cli_catalog_lifecycle_test.go @@ -111,31 +111,34 @@ func TestE2ECLIAgentTemplateLifecycle(t *testing.T) { run := func(args ...string) string { return runKagentCLI(t, t.Context(), binary, append(append([]string{}, baseArgs...), args...)...) } - writeManifest := func(systemPrompt string) { + writeManifest := func(systemPrompt, runtime string) { t.Helper() manifest := fmt.Sprintf(`apiVersion: kagent.dev/v1alpha3 kind: AgentTemplate metadata: name: %s labels: - kagent.dev/e2e-runtime: kagent + kagent.dev/e2e-runtime: %s spec: modelConfig: name: %s systemPrompt: %q -`, templateName, model.Name, systemPrompt) +`, templateName, runtime, model.Name, systemPrompt) if err := os.WriteFile(manifestPath, []byte(manifest), 0o600); err != nil { t.Fatalf("write AgentTemplate manifest: %v", err) } } - assertPrompt := func(want string) { + assertTemplate := func(wantPrompt, wantRuntime string) { t.Helper() template := &v1alpha3.AgentTemplate{} if err := kube.Get(t.Context(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template); err != nil { t.Fatalf("get AgentTemplate %q: %v", templateName, err) } - if template.Spec.SystemPrompt != want { - t.Fatalf("AgentTemplate systemPrompt = %q, want %q", template.Spec.SystemPrompt, want) + if template.Spec.SystemPrompt != wantPrompt { + t.Fatalf("AgentTemplate systemPrompt = %q, want %q", template.Spec.SystemPrompt, wantPrompt) + } + if got := template.Labels["kagent.dev/e2e-runtime"]; got != wantRuntime { + t.Fatalf("AgentTemplate runtime label = %q, want %q", got, wantRuntime) } } deleteTemplate := func() { @@ -159,20 +162,20 @@ spec: } }) - writeManifest("created through the CLI") + writeManifest("created through the CLI", "kagent") created := run("--output-format", "json", "create", "agent-template", "-f", manifestPath) if !json.Valid([]byte(created)) || !strings.Contains(created, `"name":"`+templateName+`"`) { t.Fatalf("create AgentTemplate stdout = %q, want JSON for %q", created, templateName) } - assertPrompt("created through the CLI") + assertTemplate("created through the CLI", "kagent") - writeManifest("updated through the CLI") + writeManifest("updated through the CLI", "codex") run("update", "agent-template", "-f", manifestPath) - assertPrompt("updated through the CLI") + assertTemplate("updated through the CLI", "codex") - writeManifest("applied through the CLI") + writeManifest("applied through the CLI", "kagent") run("apply", "-f", manifestPath) - assertPrompt("applied through the CLI") + assertTemplate("applied through the CLI", "kagent") deleteTemplate() @@ -180,7 +183,7 @@ spec: if !json.Valid([]byte(applied)) || !strings.Contains(applied, `"name":"`+templateName+`"`) { t.Fatalf("apply AgentTemplate stdout = %q, want JSON for %q", applied, templateName) } - assertPrompt("applied through the CLI") + assertTemplate("applied through the CLI", "kagent") deleteTemplate() } From 7aadb24bdfb9d6dc5eaa6653b690b859ddde5a10 Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Mon, 31 Aug 2026 18:51:52 -0700 Subject: [PATCH 4/7] refactor(cli): manage AgentTemplates through apply Signed-off-by: Cody Hartsook --- .../cli/internal/commands/agent_template.go | 40 ------------------- .../internal/commands/agent_template_test.go | 10 +---- go/core/cli/root.go | 4 -- go/core/cli/root_test.go | 17 ++++---- .../test/e2e/cli_catalog_lifecycle_test.go | 18 ++++----- 5 files changed, 18 insertions(+), 71 deletions(-) diff --git a/go/core/cli/internal/commands/agent_template.go b/go/core/cli/internal/commands/agent_template.go index 66cecad37..3cef23c36 100644 --- a/go/core/cli/internal/commands/agent_template.go +++ b/go/core/cli/internal/commands/agent_template.go @@ -110,36 +110,6 @@ func readAgentTemplateManifest(filename, namespace string) (*apiv1alpha1.Resourc return &apiv1alpha1.ResourceReference{Namespace: namespace, Name: manifest.Name}, resource, nil } -func createAgentTemplate( - ctx context.Context, - client agentTemplateLifecycleClient, - ref *apiv1alpha1.ResourceReference, - resource *apiv1alpha1.StructuredObject, - format clioutput.Format, - out io.Writer, -) error { - response, err := client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{Ref: ref, Resource: resource}) - if err != nil { - return fmt.Errorf("create AgentTemplate: %w", err) - } - return writeAgentTemplateResult(out, format, response, response.GetAgentTemplate()) -} - -func updateAgentTemplate( - ctx context.Context, - client agentTemplateLifecycleClient, - ref *apiv1alpha1.ResourceReference, - resource *apiv1alpha1.StructuredObject, - format clioutput.Format, - out io.Writer, -) error { - response, err := client.UpdateAgentTemplate(ctx, &apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}) - if err != nil { - return fmt.Errorf("update AgentTemplate: %w", err) - } - return writeAgentTemplateResult(out, format, response, response.GetAgentTemplate()) -} - func applyAgentTemplate( ctx context.Context, client agentTemplateLifecycleClient, @@ -320,16 +290,6 @@ func NewGetAgentTemplateCmd() *cobra.Command { return cmd } -// NewCreateAgentTemplateCmd constructs the AgentTemplate create command. -func NewCreateAgentTemplateCmd() *cobra.Command { - return newAgentTemplateManifestCmd("agent-template", "Create an AgentTemplate", createAgentTemplate) -} - -// NewUpdateAgentTemplateCmd constructs the AgentTemplate update command. -func NewUpdateAgentTemplateCmd() *cobra.Command { - return newAgentTemplateManifestCmd("agent-template", "Update an AgentTemplate", updateAgentTemplate) -} - // NewApplyAgentTemplateCmd constructs the AgentTemplate apply command. func NewApplyAgentTemplateCmd() *cobra.Command { return newAgentTemplateManifestCmd("apply -f FILE", "Create or update an AgentTemplate", applyAgentTemplate) diff --git a/go/core/cli/internal/commands/agent_template_test.go b/go/core/cli/internal/commands/agent_template_test.go index 11ba8ee96..3bcd46310 100644 --- a/go/core/cli/internal/commands/agent_template_test.go +++ b/go/core/cli/internal/commands/agent_template_test.go @@ -161,20 +161,14 @@ func TestAgentTemplateLifecycleCommands(t *testing.T) { ref := template.GetRef() resource := template.GetResource() - t.Run("create", func(t *testing.T) { + t.Run("apply creates", func(t *testing.T) { client := &recordingAgentTemplateClient{template: template} var output bytes.Buffer - require.NoError(t, createAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatTable, &output)) + require.NoError(t, applyAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatTable, &output)) assert.True(t, proto.Equal(&apiv1alpha1.CreateAgentTemplateRequest{Ref: ref, Resource: resource}, client.createRequest)) assert.Contains(t, output.String(), "researcher") }) - t.Run("update", func(t *testing.T) { - client := &recordingAgentTemplateClient{template: template} - require.NoError(t, updateAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatJSON, &bytes.Buffer{})) - assert.True(t, proto.Equal(&apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}, client.updateRequest)) - }) - t.Run("apply updates existing", func(t *testing.T) { client := &recordingAgentTemplateClient{template: template, createErr: status.Error(codes.AlreadyExists, "exists")} require.NoError(t, applyAgentTemplate(t.Context(), client, ref, resource, clioutput.FormatTable, &bytes.Buffer{})) diff --git a/go/core/cli/root.go b/go/core/cli/root.go index 643473b8f..f688e46ac 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -28,21 +28,17 @@ func Root() *cobra.Command { getCmd := newResourceGroupCmd("get", "Get a kagent resource") createCmd := newResourceGroupCmd("create", "Create a kagent resource") - updateCmd := newResourceGroupCmd("update", "Update a kagent resource") deleteCmd := newResourceGroupCmd("delete", "Delete a kagent resource") getCmd.AddCommand(agentinstancecli.NewGetCmd()) getCmd.AddCommand(commands.NewGetAgentTemplateCmd()) createCmd.AddCommand(agentinstancecli.NewCreateCmd()) - createCmd.AddCommand(commands.NewCreateAgentTemplateCmd()) - updateCmd.AddCommand(commands.NewUpdateAgentTemplateCmd()) deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd()) deleteCmd.AddCommand(commands.NewDeleteAgentTemplateCmd()) rootCmd.AddCommand( getCmd, createCmd, - updateCmd, deleteCmd, commands.NewApplyAgentTemplateCmd(), agentinstancecli.NewInvokeCmd(), diff --git a/go/core/cli/root_test.go b/go/core/cli/root_test.go index 1a6f5c58d..0f1b3c19d 100644 --- a/go/core/cli/root_test.go +++ b/go/core/cli/root_test.go @@ -124,12 +124,6 @@ func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { require.NoError(t, err) assert.Equal(t, "agent-instance ID", deleteInstanceCmd.Use) - for _, command := range [][]string{{"create", "agent-template"}, {"update", "agent-template"}} { - cmd, _, err := rootCmd.Find(command) - require.NoError(t, err) - assert.Equal(t, "agent-template", cmd.Use) - assert.NotNil(t, cmd.Flags().Lookup("file")) - } applyCmd, _, err := rootCmd.Find([]string{"apply"}) require.NoError(t, err) assert.Equal(t, "apply -f FILE", applyCmd.Use) @@ -154,8 +148,14 @@ func TestRootCommandRemovesLegacyPaths(t *testing.T) { for _, command := range []string{"deploy", "init", "build", "run", "add-mcp"} { assert.NotContains(t, rootCommands, command) } + assert.NotContains(t, rootCommands, "update") assert.Contains(t, rootCommands, "mcp") + createCmd, _, err := rootCmd.Find([]string{"create"}) + require.NoError(t, err) + assert.Len(t, createCmd.Commands(), 1) + assert.Equal(t, "agent-instance", createCmd.Commands()[0].Name()) + getCmd, _, err := rootCmd.Find([]string{"get"}) require.NoError(t, err) getCommands := make([]string, 0, len(getCmd.Commands())) @@ -187,9 +187,7 @@ func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { "get agent-instance": {"get", "agent-instance"}, "get agent-template": {"get", "agent-template"}, "create agent-instance": {"create", "agent-instance", "--harness", "kagent", "--agent-template", "example"}, - "create agent-template": {"create", "agent-template", "--file", "template.yaml"}, "apply agent-template": {"apply", "--file", "template.yaml"}, - "update agent-template": {"update", "agent-template", "--file", "template.yaml"}, "delete agent-instance": {"delete", "agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab"}, "delete agent-template": {"delete", "agent-template", "example"}, "invoke": {"invoke", "--agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab", "--task", "hello"}, @@ -211,8 +209,7 @@ func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { func TestRootResourceGroupsNameAvailableTypes(t *testing.T) { for name, want := range map[string]string{ "get": "agent-instance, agent-template", - "create": "agent-instance, agent-template", - "update": "agent-template", + "create": "agent-instance", "delete": "agent-instance, agent-template", } { t.Run(name, func(t *testing.T) { diff --git a/go/core/test/e2e/cli_catalog_lifecycle_test.go b/go/core/test/e2e/cli_catalog_lifecycle_test.go index 7ea966855..eb2cfef63 100644 --- a/go/core/test/e2e/cli_catalog_lifecycle_test.go +++ b/go/core/test/e2e/cli_catalog_lifecycle_test.go @@ -162,16 +162,16 @@ spec: } }) - writeManifest("created through the CLI", "kagent") - created := run("--output-format", "json", "create", "agent-template", "-f", manifestPath) - if !json.Valid([]byte(created)) || !strings.Contains(created, `"name":"`+templateName+`"`) { - t.Fatalf("create AgentTemplate stdout = %q, want JSON for %q", created, templateName) + writeManifest("applied through the CLI", "kagent") + applied := run("--output-format", "json", "apply", "-f", manifestPath) + if !json.Valid([]byte(applied)) || !strings.Contains(applied, `"name":"`+templateName+`"`) { + t.Fatalf("apply AgentTemplate stdout = %q, want JSON for %q", applied, templateName) } - assertTemplate("created through the CLI", "kagent") + assertTemplate("applied through the CLI", "kagent") - writeManifest("updated through the CLI", "codex") - run("update", "agent-template", "-f", manifestPath) - assertTemplate("updated through the CLI", "codex") + writeManifest("reapplied through the CLI", "codex") + run("apply", "-f", manifestPath) + assertTemplate("reapplied through the CLI", "codex") writeManifest("applied through the CLI", "kagent") run("apply", "-f", manifestPath) @@ -179,7 +179,7 @@ spec: deleteTemplate() - applied := run("--output-format", "json", "apply", "-f", manifestPath) + applied = run("--output-format", "json", "apply", "-f", manifestPath) if !json.Valid([]byte(applied)) || !strings.Contains(applied, `"name":"`+templateName+`"`) { t.Fatalf("apply AgentTemplate stdout = %q, want JSON for %q", applied, templateName) } From 0e2119887dd65f747ee64737bea300f50f070e8d Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 2 Sep 2026 12:26:54 -0700 Subject: [PATCH 5/7] refactor(cli): limit AgentTemplate lifecycle to apply Signed-off-by: Cody Hartsook --- go/api/client/agenttemplate.go | 9 --- go/api/client/agenttemplategrpc_test.go | 15 +---- .../cli/internal/commands/agent_template.go | 65 ------------------- .../internal/commands/agent_template_test.go | 14 ---- go/core/cli/root.go | 1 - go/core/cli/root_test.go | 7 +- .../test/e2e/cli_catalog_lifecycle_test.go | 26 -------- 7 files changed, 2 insertions(+), 135 deletions(-) diff --git a/go/api/client/agenttemplate.go b/go/api/client/agenttemplate.go index 4c7476bd1..48ff8b2cc 100644 --- a/go/api/client/agenttemplate.go +++ b/go/api/client/agenttemplate.go @@ -34,15 +34,6 @@ func (c *AgentTemplateClient) UpdateAgentTemplate(ctx context.Context, request * return client.UpdateAgentTemplate(callContext, request) } -func (c *AgentTemplateClient) DeleteAgentTemplate(ctx context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { - client, callContext, cancel, err := c.client.agentTemplateCall(ctx) - if err != nil { - return nil, err - } - defer cancel() - return client.DeleteAgentTemplate(callContext, request) -} - func (c *BaseClient) agentTemplateCall(ctx context.Context) (apiv1alpha1.AgentTemplateServiceClient, context.Context, context.CancelFunc, error) { connection, err := c.grpcConnection() if err != nil { diff --git a/go/api/client/agenttemplategrpc_test.go b/go/api/client/agenttemplategrpc_test.go index c1ed07d2a..2c55bbff0 100644 --- a/go/api/client/agenttemplategrpc_test.go +++ b/go/api/client/agenttemplategrpc_test.go @@ -23,7 +23,6 @@ type recordingAgentTemplateService struct { observations []callObservation create *apiv1alpha1.CreateAgentTemplateRequest update *apiv1alpha1.UpdateAgentTemplateRequest - delete *apiv1alpha1.DeleteAgentTemplateRequest } func (s *recordingAgentTemplateService) CreateAgentTemplate(ctx context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { @@ -42,14 +41,6 @@ func (s *recordingAgentTemplateService) UpdateAgentTemplate(ctx context.Context, return &apiv1alpha1.UpdateAgentTemplateResponse{}, nil } -func (s *recordingAgentTemplateService) DeleteAgentTemplate(ctx context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { - s.observe(ctx) - s.mu.Lock() - defer s.mu.Unlock() - s.delete = request - return &apiv1alpha1.DeleteAgentTemplateResponse{}, nil -} - func (s *recordingAgentTemplateService) observe(ctx context.Context) { values, _ := metadata.FromIncomingContext(ctx) _, hasDeadline := ctx.Deadline() @@ -85,13 +76,9 @@ func TestAgentTemplateClientUsesGeneratedGRPC(t *testing.T) { require.NoError(t, err) _, err = clientSet.AgentTemplate.UpdateAgentTemplate(t.Context(), &apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref}) require.NoError(t, err) - _, err = clientSet.AgentTemplate.DeleteAgentTemplate(t.Context(), &apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}) - require.NoError(t, err) - service.mu.Lock() defer service.mu.Unlock() assert.True(t, proto.Equal(ref, service.create.GetRef())) assert.True(t, proto.Equal(ref, service.update.GetRef())) - assert.True(t, proto.Equal(ref, service.delete.GetRef())) - assert.Equal(t, []callObservation{{userID: "caller", hasDeadline: true}, {userID: "caller", hasDeadline: true}, {userID: "caller", hasDeadline: true}}, service.observations) + assert.Equal(t, []callObservation{{userID: "caller", hasDeadline: true}, {userID: "caller", hasDeadline: true}}, service.observations) } diff --git a/go/core/cli/internal/commands/agent_template.go b/go/core/cli/internal/commands/agent_template.go index 3cef23c36..71ba71bd6 100644 --- a/go/core/cli/internal/commands/agent_template.go +++ b/go/core/cli/internal/commands/agent_template.go @@ -46,16 +46,9 @@ type AgentTemplateManifestCfg struct { File string } -// AgentTemplateDeleteCfg configures AgentTemplate deletion. -type AgentTemplateDeleteCfg struct { - OutputFormat string - Name string -} - type agentTemplateLifecycleClient interface { CreateAgentTemplate(context.Context, *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) UpdateAgentTemplate(context.Context, *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) - DeleteAgentTemplate(context.Context, *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) } type agentTemplateManifestOperation func(context.Context, agentTemplateLifecycleClient, *apiv1alpha1.ResourceReference, *apiv1alpha1.StructuredObject, clioutput.Format, io.Writer) error @@ -132,25 +125,6 @@ func applyAgentTemplate( return writeAgentTemplateResult(out, format, updated, updated.GetAgentTemplate()) } -func deleteAgentTemplate(ctx context.Context, client agentTemplateLifecycleClient, namespace string, cfg *AgentTemplateDeleteCfg, format clioutput.Format, out io.Writer) error { - response, err := client.DeleteAgentTemplate(ctx, &apiv1alpha1.DeleteAgentTemplateRequest{ - Ref: &apiv1alpha1.ResourceReference{Namespace: namespace, Name: cfg.Name}, - }) - if err != nil { - return fmt.Errorf("delete AgentTemplate: %w", err) - } - if format == clioutput.FormatJSON { - return clioutput.WriteProto(out, response) - } - tw := table.NewWriter() - tw.AppendHeader(table.Row{"NAME", "STATUS"}) - tw.AppendRow(table.Row{cfg.Name, "DELETED"}) - if _, err := fmt.Fprintln(out, tw.Render()); err != nil { - return fmt.Errorf("write AgentTemplate output: %w", err) - } - return nil -} - func writeAgentTemplateResult(w io.Writer, format clioutput.Format, response proto.Message, result *apiv1alpha1.AgentTemplate) error { if result == nil { return errors.New("AgentTemplate operation returned no AgentTemplate") @@ -318,42 +292,3 @@ func newAgentTemplateManifestCmd(use, short string, operation agentTemplateManif _ = cmd.MarkFlagRequired("file") return cmd } - -// NewDeleteAgentTemplateCmd constructs the AgentTemplate delete command. -func NewDeleteAgentTemplateCmd() *cobra.Command { - cfg := &AgentTemplateDeleteCfg{} - cmd := &cobra.Command{ - Use: "agent-template NAME", - Short: "Delete an AgentTemplate", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) (err error) { - options, err := connection.OptionsFromCommand(cmd) - if err != nil { - return err - } - format, err := clioutput.FromCommand(cmd) - if err != nil { - return err - } - cfg.OutputFormat = format - cfg.Name = args[0] - return runDeleteAgentTemplate(cmd.Context(), options, cfg, cmd.OutOrStdout()) - }, - } - return cmd -} - -func runDeleteAgentTemplate(ctx context.Context, options connection.Options, cfg *AgentTemplateDeleteCfg, out io.Writer) (err error) { - format, err := clioutput.Parse(cfg.OutputFormat) - if err != nil { - return err - } - session, err := connection.Open(ctx, options) - if err != nil { - return err - } - defer func() { - err = errors.Join(err, session.Close()) - }() - return deleteAgentTemplate(ctx, session.Client.AgentTemplate, session.Namespace, cfg, format, out) -} diff --git a/go/core/cli/internal/commands/agent_template_test.go b/go/core/cli/internal/commands/agent_template_test.go index 3bcd46310..c67602356 100644 --- a/go/core/cli/internal/commands/agent_template_test.go +++ b/go/core/cli/internal/commands/agent_template_test.go @@ -176,14 +176,6 @@ func TestAgentTemplateLifecycleCommands(t *testing.T) { assert.True(t, proto.Equal(&apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}, client.updateRequest)) }) - t.Run("delete", func(t *testing.T) { - client := &recordingAgentTemplateClient{} - var output bytes.Buffer - require.NoError(t, deleteAgentTemplate(t.Context(), client, "team-a", &AgentTemplateDeleteCfg{Name: "researcher"}, clioutput.FormatTable, &output)) - assert.True(t, proto.Equal(&apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}, client.deleteRequest)) - assert.Contains(t, output.String(), "researcher") - assert.Contains(t, output.String(), "DELETED") - }) } func testAgentTemplateMessage(t *testing.T) *apiv1alpha1.AgentTemplate { @@ -206,7 +198,6 @@ type recordingAgentTemplateClient struct { createErr error createRequest *apiv1alpha1.CreateAgentTemplateRequest updateRequest *apiv1alpha1.UpdateAgentTemplateRequest - deleteRequest *apiv1alpha1.DeleteAgentTemplateRequest } func (c *recordingAgentTemplateClient) CreateAgentTemplate(_ context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { @@ -221,8 +212,3 @@ func (c *recordingAgentTemplateClient) UpdateAgentTemplate(_ context.Context, re c.updateRequest = request return &apiv1alpha1.UpdateAgentTemplateResponse{AgentTemplate: c.template}, nil } - -func (c *recordingAgentTemplateClient) DeleteAgentTemplate(_ context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { - c.deleteRequest = request - return &apiv1alpha1.DeleteAgentTemplateResponse{}, nil -} diff --git a/go/core/cli/root.go b/go/core/cli/root.go index f688e46ac..e4eed85ee 100644 --- a/go/core/cli/root.go +++ b/go/core/cli/root.go @@ -34,7 +34,6 @@ func Root() *cobra.Command { getCmd.AddCommand(commands.NewGetAgentTemplateCmd()) createCmd.AddCommand(agentinstancecli.NewCreateCmd()) deleteCmd.AddCommand(agentinstancecli.NewDeleteCmd()) - deleteCmd.AddCommand(commands.NewDeleteAgentTemplateCmd()) rootCmd.AddCommand( getCmd, diff --git a/go/core/cli/root_test.go b/go/core/cli/root_test.go index 0f1b3c19d..f3d6b6119 100644 --- a/go/core/cli/root_test.go +++ b/go/core/cli/root_test.go @@ -128,10 +128,6 @@ func TestRootCommandV2CatalogAndLifecycleContract(t *testing.T) { require.NoError(t, err) assert.Equal(t, "apply -f FILE", applyCmd.Use) assert.NotNil(t, applyCmd.Flags().Lookup("file")) - deleteTemplateCmd, _, err := rootCmd.Find([]string{"delete", "agent-template"}) - require.NoError(t, err) - assert.Equal(t, "agent-template NAME", deleteTemplateCmd.Use) - for _, command := range []string{"suspend", "resume"} { _, _, err := rootCmd.Find([]string{command, "agent-instance"}) assert.Error(t, err, "%s must not be exposed by the CLI", command) @@ -189,7 +185,6 @@ func TestRootCommandOutputFormatReachesResourceCommands(t *testing.T) { "create agent-instance": {"create", "agent-instance", "--harness", "kagent", "--agent-template", "example"}, "apply agent-template": {"apply", "--file", "template.yaml"}, "delete agent-instance": {"delete", "agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab"}, - "delete agent-template": {"delete", "agent-template", "example"}, "invoke": {"invoke", "--agent-instance", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab", "--task", "hello"}, } { t.Run(name, func(t *testing.T) { @@ -210,7 +205,7 @@ func TestRootResourceGroupsNameAvailableTypes(t *testing.T) { for name, want := range map[string]string{ "get": "agent-instance, agent-template", "create": "agent-instance", - "delete": "agent-instance, agent-template", + "delete": "agent-instance", } { t.Run(name, func(t *testing.T) { rootCmd := cli.Root() diff --git a/go/core/test/e2e/cli_catalog_lifecycle_test.go b/go/core/test/e2e/cli_catalog_lifecycle_test.go index eb2cfef63..82b27aaab 100644 --- a/go/core/test/e2e/cli_catalog_lifecycle_test.go +++ b/go/core/test/e2e/cli_catalog_lifecycle_test.go @@ -15,7 +15,6 @@ import ( apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" v1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" "google.golang.org/protobuf/encoding/protojson" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/clientcmd" ) @@ -141,18 +140,6 @@ spec: t.Fatalf("AgentTemplate runtime label = %q, want %q", got, wantRuntime) } } - deleteTemplate := func() { - t.Helper() - output := run("--output-format", "json", "delete", "agent-template", templateName) - if !json.Valid([]byte(output)) { - t.Fatalf("delete AgentTemplate stdout = %q, want JSON", output) - } - template := &v1alpha3.AgentTemplate{} - err := kube.Get(t.Context(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template) - if !apierrors.IsNotFound(err) { - t.Fatalf("get deleted AgentTemplate error = %v, want NotFound", err) - } - } t.Cleanup(func() { template := &v1alpha3.AgentTemplate{} if err := kube.Get(context.Background(), types.NamespacedName{Namespace: "kagent", Name: templateName}, template); err == nil { @@ -172,19 +159,6 @@ spec: writeManifest("reapplied through the CLI", "codex") run("apply", "-f", manifestPath) assertTemplate("reapplied through the CLI", "codex") - - writeManifest("applied through the CLI", "kagent") - run("apply", "-f", manifestPath) - assertTemplate("applied through the CLI", "kagent") - - deleteTemplate() - - applied = run("--output-format", "json", "apply", "-f", manifestPath) - if !json.Valid([]byte(applied)) || !strings.Contains(applied, `"name":"`+templateName+`"`) { - t.Fatalf("apply AgentTemplate stdout = %q, want JSON for %q", applied, templateName) - } - assertTemplate("applied through the CLI", "kagent") - deleteTemplate() } func TestE2ECLIAgentInstanceDiscoveryAndInvoke(t *testing.T) { From a328ef2916ca0087f9aa396942f7cd930b5ae45d Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 2 Sep 2026 12:59:47 -0700 Subject: [PATCH 6/7] refactor(cli): use local lifecycle client name Signed-off-by: Cody Hartsook --- go/core/cli/internal/commands/agent_template.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/core/cli/internal/commands/agent_template.go b/go/core/cli/internal/commands/agent_template.go index 71ba71bd6..eafd8b047 100644 --- a/go/core/cli/internal/commands/agent_template.go +++ b/go/core/cli/internal/commands/agent_template.go @@ -46,12 +46,12 @@ type AgentTemplateManifestCfg struct { File string } -type agentTemplateLifecycleClient interface { +type lifecycleClient interface { CreateAgentTemplate(context.Context, *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) UpdateAgentTemplate(context.Context, *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) } -type agentTemplateManifestOperation func(context.Context, agentTemplateLifecycleClient, *apiv1alpha1.ResourceReference, *apiv1alpha1.StructuredObject, clioutput.Format, io.Writer) error +type agentTemplateManifestOperation func(context.Context, lifecycleClient, *apiv1alpha1.ResourceReference, *apiv1alpha1.StructuredObject, clioutput.Format, io.Writer) error func runAgentTemplateManifest( ctx context.Context, @@ -105,7 +105,7 @@ func readAgentTemplateManifest(filename, namespace string) (*apiv1alpha1.Resourc func applyAgentTemplate( ctx context.Context, - client agentTemplateLifecycleClient, + client lifecycleClient, ref *apiv1alpha1.ResourceReference, resource *apiv1alpha1.StructuredObject, format clioutput.Format, From 45ffb4e8fa1fa977ee9c6548af1f90f6c9de3c1c Mon Sep 17 00:00:00 2001 From: Cody Hartsook Date: Wed, 2 Sep 2026 13:57:10 -0700 Subject: [PATCH 7/7] fix(cli): align AgentTemplate test with corev1 LocalObjectReference Signed-off-by: Cody Hartsook --- go/core/cli/internal/commands/agent_template_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/core/cli/internal/commands/agent_template_test.go b/go/core/cli/internal/commands/agent_template_test.go index c67602356..7b42444ce 100644 --- a/go/core/cli/internal/commands/agent_template_test.go +++ b/go/core/cli/internal/commands/agent_template_test.go @@ -18,6 +18,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8stesting "k8s.io/client-go/testing" @@ -175,7 +176,6 @@ func TestAgentTemplateLifecycleCommands(t *testing.T) { assert.NotNil(t, client.createRequest) assert.True(t, proto.Equal(&apiv1alpha1.UpdateAgentTemplateRequest{Ref: ref, Resource: resource}, client.updateRequest)) }) - } func testAgentTemplateMessage(t *testing.T) *apiv1alpha1.AgentTemplate { @@ -183,7 +183,7 @@ func testAgentTemplateMessage(t *testing.T) *apiv1alpha1.AgentTemplate { template := &apiv1alpha3.AgentTemplate{ TypeMeta: metav1.TypeMeta{APIVersion: apiv1alpha3.GroupVersion.String(), Kind: agentTemplateKind}, ObjectMeta: metav1.ObjectMeta{Namespace: "team-a", Name: "researcher"}, - Spec: apiv1alpha3.AgentTemplateSpec{ModelConfig: apiv1alpha3.AgentTemplateLocalReference{Name: "default"}}, + Spec: apiv1alpha3.AgentTemplateSpec{ModelConfig: &corev1.LocalObjectReference{Name: "default"}}, } resource, err := structuredobject.FromGo(template, apiv1alpha3.GroupVersion.String(), agentTemplateKind, 0) require.NoError(t, err)