Files
kandev/apps/backend/internal/agent/runtime/lifecycle/skill/deployer.go
T

108 lines
4.0 KiB
Go

package skill
import (
"context"
"errors"
"go.uber.org/zap"
settingsmodels "github.com/kandev/kandev/internal/agent/settings/models"
"github.com/kandev/kandev/internal/common/logger"
)
// MetadataKeySkillManifestJSON mirrors lifecycle.MetadataKeySkillManifestJSON
// to avoid an import cycle. It carries the JSON-serialised Manifest that
// remote workspace executors consume during setup to upload skill and
// instruction files. The lifecycle test suite asserts the same string value.
const MetadataKeySkillManifestJSON = "skill_manifest_json"
// Config holds Deployer dependencies. All fields except Logger are
// optional — a nil reader / lister produces a manifest with no skills
// or instructions, which still lets the deployer run safely (and
// short-circuit on empty content).
type Config struct {
Logger *logger.Logger
// BasePath is the kandev home directory (typically ~/.kandev).
// Files are written under <BasePath>/runtime/...
BasePath string
// SkillReader resolves skill records by ID or slug. Optional.
SkillReader SkillReader
// InstructionLister returns instruction files for an agent
// profile. Optional.
InstructionLister InstructionLister
// ProjectSkillDirResolver maps agent type ID → project-relative
// skill dir. Optional; falls back to DefaultProjectSkillDir.
ProjectSkillDirResolver ProjectSkillDirResolver
// WorkspaceSlugFn maps a workspace ID to a slug used in on-host
// runtime paths. Optional; defaults to "default" so single-user
// installs keep working without explicit wiring.
WorkspaceSlugFn func(workspaceID string) string
}
// Deployer is the runtime-tier implementation of
// lifecycle.SkillDeployer. It builds a manifest from the profile and
// dispatches delivery by executor type.
type Deployer struct {
logger *logger.Logger
basePath string
skillReader SkillReader
instructionLister InstructionLister
projectSkillDirResolver ProjectSkillDirResolver
workspaceSlugFn func(string) string
}
// New builds a Deployer. The logger is required; everything else is
// optional and degrades gracefully.
func New(cfg Config) (*Deployer, error) {
if cfg.Logger == nil {
return nil, errors.New("skill.New: logger is required")
}
wsFn := cfg.WorkspaceSlugFn
if wsFn == nil {
wsFn = func(string) string { return "default" }
}
return &Deployer{
logger: cfg.Logger.WithFields(zap.String("component", "runtime-skill-deployer")),
basePath: cfg.BasePath,
skillReader: cfg.SkillReader,
instructionLister: cfg.InstructionLister,
projectSkillDirResolver: cfg.ProjectSkillDirResolver,
workspaceSlugFn: wsFn,
}, nil
}
// Request is the input the runtime hands to a Deployer. It mirrors
// lifecycle.SkillDeployRequest but is decoupled from the lifecycle
// package to keep the import boundary one-way.
type Request struct {
Profile *settingsmodels.AgentProfile
WorkspacePath string
ExecutorType string
WorkspaceID string
SessionID string
AdditionalSkillSlugs []string
// OfficeRuntime reports whether backend selected Office mode and the
// finalized launch env contains a non-empty KANDEV_CLI. Office launch
// validation checks the remaining runtime variables before this hook runs.
// When false, system skills are omitted from the manifest. See appendSkills.
OfficeRuntime bool
}
// Deploy materialises the profile's skills and instructions into the
// workspace. It returns the metadata patches and instructions
// directory the lifecycle Manager merges onto the prepared launch
// request.
func (d *Deployer) Deploy(ctx context.Context, req Request) (DeployResult, error) {
if req.Profile == nil {
return DeployResult{}, errors.New("skill deploy: profile is required")
}
manifest := d.buildManifest(ctx, req.Profile, d.workspaceSlugFn(req.WorkspaceID), req.AdditionalSkillSlugs, req.OfficeRuntime)
result := d.deliver(ctx, manifest, req.ExecutorType, req.WorkspacePath)
return result, nil
}