diff --git a/go/api/database/client.go b/go/api/database/client.go index a76b131ac..393f9f72c 100644 --- a/go/api/database/client.go +++ b/go/api/database/client.go @@ -76,6 +76,7 @@ type Client interface { UpsertAgentTemplateHarnessPair(context.Context, AgentTemplateHarnessPair) error UpsertRuntimeRevision(context.Context, RuntimeRevision) error GetRuntimeRevision(context.Context, string) (*RuntimeRevision, error) + ListActorTemplateHarnesses(context.Context) ([]ActorTemplateHarness, error) MarkRuntimeRevisionSuccessful(context.Context, AgentTemplateHarnessPair) error RetireAgentTemplateHarnessPairs(context.Context, string, string) error RetireAgentTemplateHarnessPair(context.Context, string, string, string) error diff --git a/go/api/database/models.go b/go/api/database/models.go index 92ef9bb93..2d803dd77 100644 --- a/go/api/database/models.go +++ b/go/api/database/models.go @@ -269,6 +269,13 @@ type RuntimeRevision struct { ActorTemplateUID string } +type ActorTemplateHarness struct { + Atespace string + Name string + UID string + HarnessName string +} + // AgentInstanceQuery narrows a page of AgentInstances. Zero values mean "do not // filter on this", so an empty query lists the caller's own instances in the // namespace. diff --git a/go/core/cmd/controller-v2/main.go b/go/core/cmd/controller-v2/main.go index f2833c4e1..2c78c2c56 100644 --- a/go/core/cmd/controller-v2/main.go +++ b/go/core/cmd/controller-v2/main.go @@ -178,7 +178,10 @@ func main() { models := modelservice.NewService(manager.GetClient(), authorizer, resourceNamespace) tools := toolservice.NewService(manager.GetClient(), store, authorizer, resourceNamespace, mcpClient) prompts := prompttemplateservice.NewService(manager.GetClient(), authorizer) - system := systemservice.NewService(systemservice.WithInventory(manager.GetClient(), watchNamespaces, authorizer, actors)) + system := systemservice.NewService( + systemservice.WithInventory(manager.GetClient(), watchNamespaces, authorizer, actors), + systemservice.WithRuntimeRevisions(store), + ) feedback := feedbackservice.NewService(store) memory := memoryservice.NewService(store) instanceWorkflow := agentinstance.NewActorWorkflow(store, actors) diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index 33f0bc707..4847b9c52 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -143,6 +143,21 @@ func (c *postgresClient) GetRuntimeRevision(ctx context.Context, revision string }, nil } +func (c *postgresClient) ListActorTemplateHarnesses(ctx context.Context) ([]dbpkg.ActorTemplateHarness, error) { + rows, err := c.q.ListActorTemplateHarnesses(ctx) + if err != nil { + return nil, fmt.Errorf("list ActorTemplate harnesses: %w", err) + } + result := make([]dbpkg.ActorTemplateHarness, 0, len(rows)) + for _, row := range rows { + result = append(result, dbpkg.ActorTemplateHarness{ + Atespace: row.ActorTemplateAtespace, Name: row.ActorTemplateName, + UID: row.ActorTemplateUid, HarnessName: row.HarnessName, + }) + } + return result, nil +} + func (c *postgresClient) MarkRuntimeRevisionSuccessful(ctx context.Context, pair dbpkg.AgentTemplateHarnessPair) error { revision := pair.DesiredRevision return c.q.MarkRuntimeRevisionSuccessful(ctx, dbgen.MarkRuntimeRevisionSuccessfulParams{ diff --git a/go/core/internal/database/gen/querier.go b/go/core/internal/database/gen/querier.go index 7612f8444..59677df41 100644 --- a/go/core/internal/database/gen/querier.go +++ b/go/core/internal/database/gen/querier.go @@ -57,6 +57,7 @@ type Querier interface { InsertFeedback(ctx context.Context, arg InsertFeedbackParams) error InsertForkedAgentInstance(ctx context.Context, arg InsertForkedAgentInstanceParams) (AgentInstance, error) InsertMemory(ctx context.Context, arg InsertMemoryParams) (string, error) + ListActorTemplateHarnesses(ctx context.Context) ([]ListActorTemplateHarnessesRow, error) ListAgentInstanceCheckpointEvents(ctx context.Context, checkpointID uuid.UUID) ([]AgentInstanceTaskEvent, error) ListAgentInstanceCheckpointTasks(ctx context.Context, checkpointID uuid.UUID) ([]AgentInstanceTask, error) ListAgentInstanceCheckpoints(ctx context.Context, arg ListAgentInstanceCheckpointsParams) ([]AgentInstanceCheckpoint, error) diff --git a/go/core/internal/database/gen/runtime_revisions.sql.go b/go/core/internal/database/gen/runtime_revisions.sql.go index 88796bca4..bf984311e 100644 --- a/go/core/internal/database/gen/runtime_revisions.sql.go +++ b/go/core/internal/database/gen/runtime_revisions.sql.go @@ -53,6 +53,43 @@ func (q *Queries) GetRuntimeRevision(ctx context.Context, revision string) (Runt return i, err } +const listActorTemplateHarnesses = `-- name: ListActorTemplateHarnesses :many +SELECT actor_template_atespace, actor_template_name, actor_template_uid, harness_name +FROM runtime_revision +` + +type ListActorTemplateHarnessesRow struct { + ActorTemplateAtespace string + ActorTemplateName string + ActorTemplateUid string + HarnessName string +} + +func (q *Queries) ListActorTemplateHarnesses(ctx context.Context) ([]ListActorTemplateHarnessesRow, error) { + rows, err := q.db.Query(ctx, listActorTemplateHarnesses) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListActorTemplateHarnessesRow + for rows.Next() { + var i ListActorTemplateHarnessesRow + if err := rows.Scan( + &i.ActorTemplateAtespace, + &i.ActorTemplateName, + &i.ActorTemplateUid, + &i.HarnessName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listUnreferencedRuntimeRevisions = `-- name: ListUnreferencedRuntimeRevisions :many SELECT revision, namespace, agent_template_name, agent_template_uid, harness_name, harness_uid, source_snapshot, egress_destinations, actor_template_atespace, actor_template_name, actor_template_uid, created_at, updated_at, agent_card FROM runtime_revision r WHERE NOT EXISTS ( diff --git a/go/core/internal/database/queries/runtime_revisions.sql b/go/core/internal/database/queries/runtime_revisions.sql index 0c595dc26..47a566236 100644 --- a/go/core/internal/database/queries/runtime_revisions.sql +++ b/go/core/internal/database/queries/runtime_revisions.sql @@ -54,6 +54,10 @@ WHERE namespace = sqlc.arg(namespace) -- name: GetRuntimeRevision :one SELECT * FROM runtime_revision WHERE revision = $1; +-- name: ListActorTemplateHarnesses :many +SELECT actor_template_atespace, actor_template_name, actor_template_uid, harness_name +FROM runtime_revision; + -- name: ListUnreferencedRuntimeRevisions :many SELECT * FROM runtime_revision r WHERE NOT EXISTS ( diff --git a/go/core/internal/service/system/service.go b/go/core/internal/service/system/service.go index e3260cbf9..f1a2037b9 100644 --- a/go/core/internal/service/system/service.go +++ b/go/core/internal/service/system/service.go @@ -9,6 +9,7 @@ import ( atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + dbpkg "github.com/kagent-dev/kagent/go/api/database" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/internal/version" "github.com/kagent-dev/kagent/go/core/pkg/auth" @@ -33,11 +34,16 @@ type ATEClient interface { ListActorTemplates(context.Context, string) ([]*ateapipb.ActorTemplate, error) } +type runtimeRevisionStore interface { + ListActorTemplateHarnesses(context.Context) ([]dbpkg.ActorTemplateHarness, error) +} + type Service struct { kubeClient client.Client observedNamespaces []string authorizer auth.Authorizer ateClient ATEClient + revisions runtimeRevisionStore } type Option func(*Service) @@ -123,6 +129,12 @@ func WithInventory( } } +func WithRuntimeRevisions(revisions runtimeRevisionStore) Option { + return func(service *Service) { + service.revisions = revisions + } +} + func (s *Service) GetVersion() Version { info := version.Get() return Version{ @@ -209,6 +221,9 @@ func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace str if s.kubeClient == nil { return SubstrateStatus{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", fmt.Errorf("kubernetes client is not configured")) } + if s.revisions == nil { + return SubstrateStatus{}, serviceerrors.NewInternal("Failed to list ActorTemplate harnesses", fmt.Errorf("runtime revision store is not configured")) + } namespaces := s.substrateNamespaces(requestedNamespace) for _, namespace := range namespaces { @@ -337,6 +352,15 @@ func (s *Service) listATEState(ctx context.Context, namespaces []string) ([]Subs if err != nil { return nil, nil, nil, err } + harnessesFromDB, err := s.revisions.ListActorTemplateHarnesses(ctx) + if err != nil { + return nil, nil, nil, err + } + type templateKey struct{ atespace, name, uid string } + harnesses := make(map[templateKey]string, len(harnessesFromDB)) + for _, template := range harnessesFromDB { + harnesses[templateKey{template.Atespace, template.Name, template.UID}] = template.HarnessName + } templates := make([]SubstrateActorTemplate, 0, len(templatesFromAPI)) for _, template := range templatesFromAPI { if template == nil || !allowedAtespace(template.GetMetadata().GetAtespace(), allowAll, allowed) { @@ -349,13 +373,17 @@ func (s *Service) listATEState(ctx context.Context, namespaces []string) ([]Subs } else if golden.GetGoldenSnapshot() != nil { phase = "Ready" } + metadata := template.GetMetadata() templates = append(templates, SubstrateActorTemplate{ - Namespace: template.GetMetadata().GetAtespace(), - Name: template.GetMetadata().GetName(), - Phase: phase, - GoldenSnapshot: objectRefString(golden.GetGoldenSnapshot()), - SandboxClass: template.GetSandboxConfig().GetSandboxClass().String(), - WorkerSelector: labelSelectorString(ctx, &metav1.LabelSelector{MatchLabels: template.GetWorkerSelector().GetMatchLabels()}), + Namespace: metadata.GetAtespace(), + Name: metadata.GetName(), + Phase: phase, + GoldenActorID: metadata.GetUid(), + GoldenSnapshot: golden.GetGoldenSnapshot().GetName(), + SandboxClass: strings.ToLower(strings.TrimPrefix(template.GetSandboxConfig().GetSandboxClass().String(), "SANDBOX_CLASS_")), + WorkerSelector: labelSelectorString(ctx, &metav1.LabelSelector{MatchLabels: template.GetWorkerSelector().GetMatchLabels()}), + HarnessName: harnesses[templateKey{metadata.GetAtespace(), metadata.GetName(), metadata.GetUid()}], + ManagedByKagent: true, }) } @@ -394,13 +422,6 @@ func allowedAtespace(atespace string, allowAll bool, allowed map[string]struct{} return ok } -func objectRefString(ref *ateapipb.ObjectRef) string { - if ref == nil { - return "" - } - return ref.GetAtespace() + "/" + ref.GetName() -} - func actorFromProto(actor *ateapipb.Actor) SubstrateActor { assignment := actor.GetStatus().GetWorkerAssignment() return SubstrateActor{ diff --git a/go/core/internal/service/system/service_test.go b/go/core/internal/service/system/service_test.go index b03c599e3..d02365d22 100644 --- a/go/core/internal/service/system/service_test.go +++ b/go/core/internal/service/system/service_test.go @@ -7,6 +7,7 @@ import ( atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + dbpkg "github.com/kagent-dev/kagent/go/api/database" authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/internal/service/system" @@ -36,6 +37,14 @@ type fakeATEClient struct { err error } +type fakeRuntimeRevisionStore struct { + harnesses []dbpkg.ActorTemplateHarness +} + +func (store *fakeRuntimeRevisionStore) ListActorTemplateHarnesses(context.Context) ([]dbpkg.ActorTemplateHarness, error) { + return store.harnesses, nil +} + func (client *fakeATEClient) ListActors(context.Context, string) ([]*ateapipb.Actor, error) { if client.err != nil { return nil, client.err @@ -130,7 +139,7 @@ func TestGetSubstrateStatus(t *testing.T) { ).Build() ateClient := &fakeATEClient{ templates: []*ateapipb.ActorTemplate{{ - Metadata: &ateapipb.ResourceMetadata{Atespace: "team", Name: "template"}, + Metadata: &ateapipb.ResourceMetadata{Atespace: "team", Name: "template", Uid: "template-uid"}, SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR}, Status: &ateapipb.ActorTemplateStatus{GoldenSnapshotStatus: &ateapipb.GoldenSnapshotStatus{ GoldenSnapshot: &ateapipb.ObjectRef{Atespace: "ate-golden", Name: "golden"}, @@ -154,7 +163,13 @@ func TestGetSubstrateStatus(t *testing.T) { }}, }}, } - service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient)) + revisions := &fakeRuntimeRevisionStore{harnesses: []dbpkg.ActorTemplateHarness{{ + Atespace: "team", Name: "template", UID: "template-uid", HarnessName: "kagent", + }}} + service := system.NewService( + system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient), + system.WithRuntimeRevisions(revisions), + ) result, err := service.GetSubstrateStatus(ctx, "team") require.NoError(t, err) @@ -163,6 +178,11 @@ func TestGetSubstrateStatus(t *testing.T) { assert.Equal(t, int32(2), result.WorkerPools[0].Replicas) require.Len(t, result.ActorTemplates, 1) assert.Equal(t, "Ready", result.ActorTemplates[0].Phase) + assert.Equal(t, "template-uid", result.ActorTemplates[0].GoldenActorID) + assert.Equal(t, "golden", result.ActorTemplates[0].GoldenSnapshot) + assert.Equal(t, "gvisor", result.ActorTemplates[0].SandboxClass) + assert.Equal(t, "kagent", result.ActorTemplates[0].HarnessName) + assert.True(t, result.ActorTemplates[0].ManagedByKagent) require.Len(t, result.Actors, 1) assert.Equal(t, "Running", result.Actors[0].Status) require.Len(t, result.Workers, 1) diff --git a/ui/src/api/domain/substrate.ts b/ui/src/api/domain/substrate.ts index d5b9c9061..7207be4c6 100644 --- a/ui/src/api/domain/substrate.ts +++ b/ui/src/api/domain/substrate.ts @@ -47,7 +47,6 @@ export interface SubstrateActorTemplateEntry { sandboxClass?: string; workerSelector?: string; harnessName?: string; - managedByKagent: boolean; } /** Runtime actor state, from ate-api rather than from Kubernetes. */ diff --git a/ui/src/api/grpc/operations.ts b/ui/src/api/grpc/operations.ts index 94c01b388..42dc7fa8a 100644 --- a/ui/src/api/grpc/operations.ts +++ b/ui/src/api/grpc/operations.ts @@ -1297,7 +1297,6 @@ function toActorTemplateEntry( sandboxClass: orUndefined(template.sandboxClass), workerSelector: orUndefined(template.workerSelector), harnessName: orUndefined(template.harnessName), - managedByKagent: template.managedByKagent, }; } diff --git a/ui/src/api/operations.test.ts b/ui/src/api/operations.test.ts index 8bb3d4730..3ca11d4ba 100644 --- a/ui/src/api/operations.test.ts +++ b/ui/src/api/operations.test.ts @@ -687,9 +687,7 @@ describe("the cluster", () => { workerPools: [ { namespace: "kagent", name: "pool", replicas: 2, ateomImage: "ateom:1" }, ], - actorTemplates: [ - { namespace: "kagent", name: "tpl", managedByKagent: true, phase: "Ready" }, - ], + actorTemplates: [{ namespace: "kagent", name: "tpl", phase: "Ready" }], actors: [{ actorId: "a1", atespace: "kagent", status: "Running", version: 3n }], workers: [], }), @@ -703,7 +701,6 @@ describe("the cluster", () => { expect(status.ateApiError).toMatch(/ate-api/); expect(status.actors[0].atespace).toBe("kagent"); expect(status.actors[0].version).toBe(3); - expect(status.actorTemplates[0].managedByKagent).toBe(true); }); // Proto3 cannot tell an unset string from an empty one, and an empty warning diff --git a/ui/src/mocks/fixtures.ts b/ui/src/mocks/fixtures.ts index 0ad0d7807..668d4aef4 100644 --- a/ui/src/mocks/fixtures.ts +++ b/ui/src/mocks/fixtures.ts @@ -484,13 +484,11 @@ export const mockSubstrateStatus: SubstrateStatusResponse = { sandboxClass: "standard", workerSelector: "pool=default-pool", harnessName: "openclaw", - managedByKagent: true, }, { namespace: "platform", name: "external-template", phase: "Pending", - managedByKagent: false, }, ], actors: [ diff --git a/ui/src/mocks/transport.ts b/ui/src/mocks/transport.ts index 63afa712d..343e50e3b 100644 --- a/ui/src/mocks/transport.ts +++ b/ui/src/mocks/transport.ts @@ -1515,7 +1515,6 @@ on(SystemService.method.getSubstrateStatus, (input, call) => { sandboxClass: template.sandboxClass ?? "", workerSelector: template.workerSelector ?? "", harnessName: template.harnessName ?? "", - managedByKagent: template.managedByKagent ?? false, })), actors: actors.map((actor) => ({ actorId: actor.actorId,