Files
krow_backend/go-api/internal/runtime/loader.go
2026-08-24 13:06:29 +05:30

238 lines
6.5 KiB
Go

package runtime
import (
"context"
"errors"
"fmt"
"regexp"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/definition"
"github.com/krow/krow-backend/go-api/internal/domain"
"github.com/krow/krow-backend/go-api/internal/repo"
)
var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
func isUUID(s string) bool {
return uuidPattern.MatchString(s)
}
// Loader loads and validates authored definitions into runtime representations with tenant isolation.
type Loader struct {
repo *repo.DefinitionsRepo
}
// NewLoader builds a runtime definition loader over a storage repository.
func NewLoader(db repo.Querier) *Loader {
return &Loader{repo: repo.NewDefinitionsRepo(db)}
}
// LoadAgent loads an agent definition by id or definition_id, parsing it into a runtime representation.
func (l *Loader) LoadAgent(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Agent, error) {
var (
rec domain.Record
err error
)
if isUUID(idOrDefID) {
rec, err = l.repo.GetAgent(ctx, ident, idOrDefID)
} else {
rec, err = l.repo.GetAgentByDefinitionID(ctx, ident, idOrDefID)
}
if err != nil {
return nil, err
}
if rec == nil {
return nil, fmt.Errorf("%w: agent %q", ErrNotFound, idOrDefID)
}
rawMD, ok := rec["markdown"].(string)
if !ok || rawMD == "" {
return nil, fmt.Errorf("%w: missing markdown payload for agent %q", ErrInvalidDefinition, idOrDefID)
}
if err := definition.ValidateAgent(rawMD); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}
parsed, err := definition.ParseAgent(rawMD, definition.Options{})
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}
agent := &Agent{
ID: parsed.ID,
DatabaseID: rec["id"].(string),
Name: parsed.Name,
Description: parsed.Description,
Status: parsed.Status,
Version: parsed.Version,
Visibility: rec["visibility"].(string),
Pages: parsed.Pages,
Icon: parsed.Icon,
Reasoning: parsed.Reasoning,
Trigger: parsed.Trigger,
WebSearch: parsed.WebSearch,
Instructions: parsed.Instructions,
Skills: parsed.Skills,
Subagents: parsed.Subagents,
RawMarkdown: rawMD,
}
if rec["owner_user_id"] != nil {
if uid, ok := rec["owner_user_id"].(string); ok && uid != "" {
agent.OwnerUserID = &uid
}
}
return agent, nil
}
// LoadSkill loads a skill definition by id or definition_id, parsing it into a runtime representation.
func (l *Loader) LoadSkill(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Skill, error) {
var (
rec domain.Record
err error
)
if isUUID(idOrDefID) {
rec, err = l.repo.GetSkill(ctx, ident, idOrDefID)
} else {
rec, err = l.repo.GetSkillByDefinitionID(ctx, ident, idOrDefID)
}
if err != nil {
return nil, err
}
if rec == nil {
return nil, fmt.Errorf("%w: skill %q", ErrNotFound, idOrDefID)
}
rawMD, ok := rec["markdown"].(string)
if !ok || rawMD == "" {
return nil, fmt.Errorf("%w: missing markdown payload for skill %q", ErrInvalidDefinition, idOrDefID)
}
if err := definition.ValidateSkill(rawMD); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}
parsed, err := definition.ParseSkill(rawMD, definition.Options{})
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}
skill := &Skill{
ID: parsed.ID,
DatabaseID: rec["id"].(string),
Name: parsed.Name,
Description: parsed.Description,
Status: parsed.Status,
Visibility: rec["visibility"].(string),
Pages: parsed.Pages,
Kind: parsed.Kind,
Category: parsed.Category,
Actions: parsed.Actions,
Triggers: parsed.Triggers,
Prompt: parsed.Prompt,
SkillID: parsed.SkillID,
Body: parsed.Body,
RawMarkdown: rawMD,
}
if rec["owner_user_id"] != nil {
if uid, ok := rec["owner_user_id"].(string); ok && uid != "" {
skill.OwnerUserID = &uid
}
}
return skill, nil
}
// ResolveAgentDependencies resolves all skill dependencies referenced by the agent within caller scope.
func (l *Loader) ResolveAgentDependencies(ctx context.Context, ident authctx.Identity, agent *Agent) error {
if len(agent.Skills) == 0 {
agent.ResolvedSkills = []*Skill{}
return nil
}
visited := make(map[string]*Skill)
inProgress := make(map[string]bool)
resolved := make([]*Skill, 0, len(agent.Skills))
for _, skillID := range agent.Skills {
if _, ok := visited[skillID]; ok {
// Deterministic deduplication
continue
}
if inProgress[skillID] {
return fmt.Errorf("%w: skill %q", ErrCircularDependency, skillID)
}
inProgress[skillID] = true
skill, err := l.LoadSkill(ctx, ident, skillID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return fmt.Errorf("%w: skill %q", ErrDependencyMissing, skillID)
}
return err
}
if skill.Status != "active" {
return fmt.Errorf("%w: skill %q has status %q", ErrDependencyInactive, skillID, skill.Status)
}
inProgress[skillID] = false
visited[skillID] = skill
resolved = append(resolved, skill)
}
agent.ResolvedSkills = resolved
return nil
}
// LoadExecutableAgent loads an agent, verifies its published status, and resolves all active dependencies.
func (l *Loader) LoadExecutableAgent(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Agent, error) {
agent, err := l.LoadAgent(ctx, ident, idOrDefID)
if err != nil {
return nil, err
}
switch agent.Status {
case "published":
// Eligible
case "draft":
return nil, fmt.Errorf("%w: agent %q is in draft status", ErrDraftAgent, agent.ID)
case "archived":
return nil, fmt.Errorf("%w: agent %q is archived", ErrArchivedAgent, agent.ID)
default:
return nil, fmt.Errorf("%w: agent %q has unsupported status %q", ErrNotExecutable, agent.ID, agent.Status)
}
if err := l.ResolveAgentDependencies(ctx, ident, agent); err != nil {
return nil, err
}
return agent, nil
}
// LoadExecutableSkill loads a skill and verifies its active status.
func (l *Loader) LoadExecutableSkill(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Skill, error) {
skill, err := l.LoadSkill(ctx, ident, idOrDefID)
if err != nil {
return nil, err
}
switch skill.Status {
case "active":
// Eligible
case "inactive":
return nil, fmt.Errorf("%w: skill %q is inactive", ErrInactiveSkill, skill.ID)
default:
return nil, fmt.Errorf("%w: skill %q has unsupported status %q", ErrNotExecutable, skill.ID, skill.Status)
}
return skill, nil
}