Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go/api/database/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions go/api/database/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion go/core/cmd/controller-v2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions go/core/internal/database/client_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
1 change: 1 addition & 0 deletions go/core/internal/database/gen/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions go/core/internal/database/gen/runtime_revisions.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions go/core/internal/database/queries/runtime_revisions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
47 changes: 34 additions & 13 deletions go/core/internal/service/system/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
})
}

Expand Down Expand Up @@ -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{
Expand Down
24 changes: 22 additions & 2 deletions go/core/internal/service/system/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"},
Expand All @@ -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)
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion ui/src/api/domain/substrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
1 change: 0 additions & 1 deletion ui/src/api/grpc/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,7 +1297,6 @@ function toActorTemplateEntry(
sandboxClass: orUndefined(template.sandboxClass),
workerSelector: orUndefined(template.workerSelector),
harnessName: orUndefined(template.harnessName),
managedByKagent: template.managedByKagent,
};
}

Expand Down
5 changes: 1 addition & 4 deletions ui/src/api/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
}),
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions ui/src/mocks/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
1 change: 0 additions & 1 deletion ui/src/mocks/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading