first commit
This commit is contained in:
237
go-api/internal/runtime/loader.go
Normal file
237
go-api/internal/runtime/loader.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user