Files
krow_backend/go-api/internal/runtime/loader.go
2026-08-28 12:21:44 +05:30

306 lines
9.2 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
// versions resolves a pinned version back to the definition that answered.
// See LoadAgentVersion.
versions *repo.VersionsRepo
}
// NewLoader builds a runtime definition loader over a storage repository.
func NewLoader(db repo.Querier) *Loader {
return &Loader{
repo: repo.NewDefinitionsRepo(db),
versions: repo.NewVersionsRepo(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,
Tools: parsed.Tools,
// The spec's corpora, and the ONLY place they come from. A model that
// asked to search a source its agent was not granted is asking for a
// list it has no way to set — see tools.Context.KnowledgeSources.
KnowledgeSources: parsed.Sources,
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
}
/* ── Pinned versions ────────────────────────────────────────────────────── */
// LoadAgentVersion loads an agent AS IT WAS at a published version.
//
// The current definition is not consulted at all — that is the point. An agent
// edited since a conversation began is a different agent, and a run that quietly
// switched to it would answer a question the reader never asked with tools they
// were never offered.
//
// Falls back to the current definition when the version is not in the history,
// and does so deliberately rather than failing. Every definition published
// before migration 000010 has no snapshot; refusing those would break every
// existing conversation to enforce a rule that could not have been followed
// when they started. The fallback is recorded by the caller, so a run that
// could not pin is visible rather than silent.
func (l *Loader) LoadAgentVersion(ctx context.Context, ident authctx.Identity,
idOrDefID string, version int) (*Agent, bool, error) {
current, err := l.LoadExecutableAgent(ctx, ident, idOrDefID)
if err != nil {
return nil, false, err
}
if version <= 0 || version == current.Version {
return current, false, nil
}
snapshot, err := l.versions.Load(ctx, ident, repo.KindAgent, current.ID, version)
if err != nil || snapshot == nil {
// No snapshot for that number. The run continues on the current
// definition — see the note above — and the caller records that it
// could not pin.
return current, true, nil
}
parsed, err := definition.ParseAgent(snapshot.Markdown, definition.Options{})
if err != nil {
return current, true, nil
}
// Rebuilt from the snapshot, keeping the identity fields that belong to the
// row rather than to the definition text.
pinned := *current
pinned.Name = parsed.Name
pinned.Description = parsed.Description
pinned.Version = snapshot.Version
pinned.Pages = parsed.Pages
pinned.Reasoning = parsed.Reasoning
pinned.Instructions = parsed.Instructions
pinned.Skills = parsed.Skills
pinned.Tools = parsed.Tools
pinned.KnowledgeSources = parsed.Sources
pinned.Subagents = parsed.Subagents
pinned.RawMarkdown = snapshot.Markdown
return &pinned, false, nil
}