Files
kandev/apps/backend/internal/agent/runtime/lifecycle/skill_deploy.go
T
2026-05-16 20:07:57 +01:00

78 lines
2.7 KiB
Go

package lifecycle
import (
"context"
"go.uber.org/zap"
)
// MetadataKeyInstructionsDir carries the per-launch path where the
// SkillDeployer wrote the agent's instruction files. Office's prompt
// builder reads from this path; kanban launches don't consume it.
const MetadataKeyInstructionsDir = "kandev_instructions_dir"
// runSkillDeploy resolves the agent profile (now including the office
// enrichment fields from ADR 0005 Wave A), short-circuits when there's
// nothing to deploy, and otherwise hands off to the configured
// SkillDeployer. Any metadata patches the deployer returns are merged
// onto the prepared LaunchRequest so executor backends see them
// (e.g. Docker bind-mount, Sprites manifest upload). Errors are logged
// and swallowed: an in-flight launch must not be aborted because skill
// materialisation hiccupped.
func (m *Manager) runSkillDeploy(ctx context.Context, original, prepared *LaunchRequest) {
if m.skillDeployer == nil || m.agentProfileReader == nil {
return
}
if original.AgentProfileID == "" {
return
}
profile, err := m.agentProfileReader.GetAgentProfile(ctx, original.AgentProfileID)
if err != nil || profile == nil {
m.logger.Debug("skill deploy skipped: profile lookup failed",
zap.String("profile_id", original.AgentProfileID),
zap.Error(err))
return
}
if len(profile.SkillIDs) == 0 && len(profile.DesiredSkills) == 0 {
// Fast path: shallow / kanban-flavour profiles with no enrichment to
// deploy. Most launches today land here.
return
}
req := SkillDeployRequest{
Profile: profile,
WorkspacePath: prepared.WorkspacePath,
ExecutorType: prepared.ExecutorType,
WorkspaceID: profile.WorkspaceID,
SessionID: original.SessionID,
}
result, err := m.skillDeployer.DeploySkills(ctx, req)
if err != nil {
m.logger.Warn("skill deploy failed; launch continues",
zap.String("profile_id", profile.ID),
zap.String("workspace_path", prepared.WorkspacePath),
zap.Error(err))
return
}
mergeSkillMetadata(prepared, result)
}
// mergeSkillMetadata applies the deployer's output to the prepared
// launch request: every metadata patch is written through, and the
// instructions directory is stamped under MetadataKeyInstructionsDir
// so downstream consumers (office prompt builder, run detail UI) can
// recover the path.
func mergeSkillMetadata(prepared *LaunchRequest, result SkillDeployResult) {
if len(result.Metadata) == 0 && result.InstructionsDir == "" {
return
}
if prepared.Metadata == nil {
prepared.Metadata = make(map[string]interface{})
}
for k, v := range result.Metadata {
prepared.Metadata[k] = v
}
if result.InstructionsDir != "" {
prepared.Metadata[MetadataKeyInstructionsDir] = result.InstructionsDir
}
}