diff --git a/AGENTS.md b/AGENTS.md index f621f859..885c3605 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ All agent-facing guidance for this repository. Read this file first. ## What this project is A Kubernetes controller for managing AI agent workloads. Defines CRDs -(Agent, AgentRun, AgentPlaybook, SkillCard, SkillCollection, -LLMProvider) and controllers for composing and executing agent +(Agent, AgentRun, AgentWorkflow, AgentWorkflowRun, SkillCard, +SkillCollection, LLMProvider) and controllers for composing and executing agent workloads via Agent Sandbox. ## Key documents diff --git a/README.md b/README.md index 155397c4..2a65e2bd 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ CI pipeline) resolves application metadata before creating the CR. | **LLMProvider** | LLM service endpoint, credentials, and available models. | | **Agent** | Template declaring available skills, providers, container image, prompt, and typed parameters. | | **AgentRun** | Execute a single Agent with specific values. Creates an Agent Sandbox. | -| **AgentPlaybook** | Ordered sequence of stages, each referencing an Agent. | -| **AgentPlaybookRun** | Execute a playbook. Creates AgentRuns sequentially per stage. | +| **AgentWorkflow** | Ordered sequence of stages, each referencing an Agent. | +| **AgentWorkflowRun** | Execute a workflow. Creates AgentRuns sequentially per stage. | ### Key design decisions diff --git a/api/v1alpha1/agentplaybook_types.go b/api/v1alpha1/agentworkflow_types.go similarity index 76% rename from api/v1alpha1/agentplaybook_types.go rename to api/v1alpha1/agentworkflow_types.go index ca91cab2..4822a2f2 100644 --- a/api/v1alpha1/agentplaybook_types.go +++ b/api/v1alpha1/agentworkflow_types.go @@ -20,10 +20,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// AgentPlaybookStage defines one stage in a playbook. +// AgentWorkflowStage defines one stage in a workflow. // Each stage references an Agent and carries instructions. -type AgentPlaybookStage struct { - // Name is the stage name, unique within the playbook. +type AgentWorkflowStage struct { + // Name is the stage name, unique within the workflow. // Must be a valid Kubernetes label value (lowercase alphanumeric, // hyphens, dots, max 63 chars) since it is used in labels on // child AgentRun resources. @@ -42,8 +42,8 @@ type AgentPlaybookStage struct { Instructions string `json:"instructions,omitempty"` } -// AgentPlaybookSpec defines the desired state of an AgentPlaybook. -type AgentPlaybookSpec struct { +// AgentWorkflowSpec defines the desired state of an AgentWorkflow. +type AgentWorkflowSpec struct { // Guide is a high-level guide providing ambient context for all stages. // Written as a context file in the workspace so each agent understands // where its work fits in the bigger picture. @@ -57,17 +57,17 @@ type AgentPlaybookSpec struct { // +kubebuilder:validation:MinItems=1 // +listType=map // +listMapKey=name - Stages []AgentPlaybookStage `json:"stages"` + Stages []AgentWorkflowStage `json:"stages"` } -// AgentPlaybookStatus defines the observed state of an AgentPlaybook. -type AgentPlaybookStatus struct { +// AgentWorkflowStatus defines the observed state of an AgentWorkflow. +type AgentWorkflowStatus struct { // ObservedGeneration is the most recent generation observed by the controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` // Conditions represent the latest available observations of the - // AgentPlaybook's state. + // AgentWorkflow's state. // +optional // +listType=map // +listMapKey=type @@ -76,27 +76,27 @@ type AgentPlaybookStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=ap +// +kubebuilder:resource:shortName=aw // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// AgentPlaybook is a reusable playbook combining a high-level guide with an +// AgentWorkflow is a reusable workflow combining a high-level guide with an // ordered sequence of stages. Each stage references an Agent and carries -// instructions. An AgentPlaybook is a template — creating one does not +// instructions. An AgentWorkflow is a template — creating one does not // execute anything. -type AgentPlaybook struct { +type AgentWorkflow struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec AgentPlaybookSpec `json:"spec,omitempty"` - Status AgentPlaybookStatus `json:"status,omitempty"` + Spec AgentWorkflowSpec `json:"spec,omitempty"` + Status AgentWorkflowStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// AgentPlaybookList contains a list of AgentPlaybook. -type AgentPlaybookList struct { +// AgentWorkflowList contains a list of AgentWorkflow. +type AgentWorkflowList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []AgentPlaybook `json:"items"` + Items []AgentWorkflow `json:"items"` } diff --git a/api/v1alpha1/agentplaybookrun_types.go b/api/v1alpha1/agentworkflowrun_types.go similarity index 72% rename from api/v1alpha1/agentplaybookrun_types.go rename to api/v1alpha1/agentworkflowrun_types.go index e0addbb8..050a7783 100644 --- a/api/v1alpha1/agentplaybookrun_types.go +++ b/api/v1alpha1/agentworkflowrun_types.go @@ -21,10 +21,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// AgentPlaybookRunStageStatus tracks the status of a single stage within -// a playbook run. -type AgentPlaybookRunStageStatus struct { - // Name is the stage name, matching a stage in the AgentPlaybook. +// AgentWorkflowRunStageStatus tracks the status of a single stage within +// a workflow run. +type AgentWorkflowRunStageStatus struct { + // Name is the stage name, matching a stage in the AgentWorkflow. Name string `json:"name"` // Phase is the current phase of this stage. @@ -35,13 +35,13 @@ type AgentPlaybookRunStageStatus struct { AgentRunName string `json:"agentRunName,omitempty"` } -// AgentPlaybookRunSpec defines the desired state of an AgentPlaybookRun. +// AgentWorkflowRunSpec defines the desired state of an AgentWorkflowRun. // The spec is immutable once created — delete and recreate to change values. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" -type AgentPlaybookRunSpec struct { - // PlaybookRef is the name of the AgentPlaybook CR to execute. +type AgentWorkflowRunSpec struct { + // WorkflowRef is the name of the AgentWorkflow CR to execute. // +kubebuilder:validation:MinLength=1 - PlaybookRef string `json:"playbookRef"` + WorkflowRef string `json:"workflowRef"` // Models selects specific provider/model combinations for all stages. // Individual stages may override these selections in the future. @@ -67,9 +67,9 @@ type AgentPlaybookRunSpec struct { EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` } -// AgentPlaybookRunStatus defines the observed state of an AgentPlaybookRun. -type AgentPlaybookRunStatus struct { - // Phase is the current phase of the overall playbook run. +// AgentWorkflowRunStatus defines the observed state of an AgentWorkflowRun. +type AgentWorkflowRunStatus struct { + // Phase is the current phase of the overall workflow run. // +kubebuilder:default=Pending // +optional Phase AgentRunPhase `json:"phase,omitempty"` @@ -86,18 +86,18 @@ type AgentPlaybookRunStatus struct { // +optional // +listType=map // +listMapKey=name - Stages []AgentPlaybookRunStageStatus `json:"stages,omitempty"` + Stages []AgentWorkflowRunStageStatus `json:"stages,omitempty"` - // StartTime is the time the playbook run started. + // StartTime is the time the workflow run started. // +optional StartTime *metav1.Time `json:"startTime,omitempty"` - // CompletionTime is the time the playbook run finished. + // CompletionTime is the time the workflow run finished. // +optional CompletionTime *metav1.Time `json:"completionTime,omitempty"` // Conditions represent the latest available observations of the - // AgentPlaybookRun's state. + // AgentWorkflowRun's state. // +optional // +listType=map // +listMapKey=type @@ -106,29 +106,29 @@ type AgentPlaybookRunStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=apr -// +kubebuilder:printcolumn:name="Playbook",type=string,JSONPath=`.spec.playbookRef` +// +kubebuilder:resource:shortName=awr +// +kubebuilder:printcolumn:name="Workflow",type=string,JSONPath=`.spec.workflowRef` // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Current Stage",type=string,JSONPath=`.status.currentStage` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` -// AgentPlaybookRun is a request to execute an AgentPlaybook. It references -// an AgentPlaybook and carries generic parameters. The controller orchestrates +// AgentWorkflowRun is a request to execute an AgentWorkflow. It references +// an AgentWorkflow and carries generic parameters. The controller orchestrates // execution: creates an AgentRun per stage, manages cross-stage handoff via // committed files on a shared target branch. -type AgentPlaybookRun struct { +type AgentWorkflowRun struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec AgentPlaybookRunSpec `json:"spec,omitempty"` - Status AgentPlaybookRunStatus `json:"status,omitempty"` + Spec AgentWorkflowRunSpec `json:"spec,omitempty"` + Status AgentWorkflowRunStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true -// AgentPlaybookRunList contains a list of AgentPlaybookRun. -type AgentPlaybookRunList struct { +// AgentWorkflowRunList contains a list of AgentWorkflowRun. +type AgentWorkflowRunList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []AgentPlaybookRun `json:"items"` + Items []AgentWorkflowRun `json:"items"` } diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 3c4e5602..7f645c8f 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -41,10 +41,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(GroupVersion, &Agent{}, &AgentList{}, - &AgentPlaybook{}, - &AgentPlaybookList{}, - &AgentPlaybookRun{}, - &AgentPlaybookRunList{}, + &AgentWorkflow{}, + &AgentWorkflowList{}, + &AgentWorkflowRun{}, + &AgentWorkflowRunList{}, &AgentRun{}, &AgentRunList{}, &LLMProvider{}, diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4e7f3724..23c25d47 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -101,7 +101,22 @@ func (in *AgentParam) DeepCopy() *AgentParam { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybook) DeepCopyInto(out *AgentPlaybook) { +func (in *AgentProviderRef) DeepCopyInto(out *AgentProviderRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentProviderRef. +func (in *AgentProviderRef) DeepCopy() *AgentProviderRef { + if in == nil { + return nil + } + out := new(AgentProviderRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentRun) DeepCopyInto(out *AgentRun) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -109,18 +124,18 @@ func (in *AgentPlaybook) DeepCopyInto(out *AgentPlaybook) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybook. -func (in *AgentPlaybook) DeepCopy() *AgentPlaybook { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRun. +func (in *AgentRun) DeepCopy() *AgentRun { if in == nil { return nil } - out := new(AgentPlaybook) + out := new(AgentRun) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybook) DeepCopyObject() runtime.Object { +func (in *AgentRun) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -128,31 +143,31 @@ func (in *AgentPlaybook) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookList) DeepCopyInto(out *AgentPlaybookList) { +func (in *AgentRunList) DeepCopyInto(out *AgentRunList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentPlaybook, len(*in)) + *out = make([]AgentRun, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookList. -func (in *AgentPlaybookList) DeepCopy() *AgentPlaybookList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunList. +func (in *AgentRunList) DeepCopy() *AgentRunList { if in == nil { return nil } - out := new(AgentPlaybookList) + out := new(AgentRunList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookList) DeepCopyObject() runtime.Object { +func (in *AgentRunList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -160,66 +175,37 @@ func (in *AgentPlaybookList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRun) DeepCopyInto(out *AgentPlaybookRun) { +func (in *AgentRunModelSelection) DeepCopyInto(out *AgentRunModelSelection) { *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRun. -func (in *AgentPlaybookRun) DeepCopy() *AgentPlaybookRun { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunModelSelection. +func (in *AgentRunModelSelection) DeepCopy() *AgentRunModelSelection { if in == nil { return nil } - out := new(AgentPlaybookRun) + out := new(AgentRunModelSelection) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookRun) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunList) DeepCopyInto(out *AgentPlaybookRunList) { +func (in *AgentRunParam) DeepCopyInto(out *AgentRunParam) { *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AgentPlaybookRun, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunList. -func (in *AgentPlaybookRunList) DeepCopy() *AgentPlaybookRunList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunParam. +func (in *AgentRunParam) DeepCopy() *AgentRunParam { if in == nil { return nil } - out := new(AgentPlaybookRunList) + out := new(AgentRunParam) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentPlaybookRunList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunSpec) DeepCopyInto(out *AgentPlaybookRunSpec) { +func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { *out = *in if in.Models != nil { in, out := &in.Models, &out.Models @@ -247,39 +233,19 @@ func (in *AgentPlaybookRunSpec) DeepCopyInto(out *AgentPlaybookRunSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunSpec. -func (in *AgentPlaybookRunSpec) DeepCopy() *AgentPlaybookRunSpec { - if in == nil { - return nil - } - out := new(AgentPlaybookRunSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunStageStatus) DeepCopyInto(out *AgentPlaybookRunStageStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunStageStatus. -func (in *AgentPlaybookRunStageStatus) DeepCopy() *AgentPlaybookRunStageStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunSpec. +func (in *AgentRunSpec) DeepCopy() *AgentRunSpec { if in == nil { return nil } - out := new(AgentPlaybookRunStageStatus) + out := new(AgentRunSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { +func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { *out = *in - if in.Stages != nil { - in, out := &in.Stages, &out.Stages - *out = make([]AgentPlaybookRunStageStatus, len(*in)) - copy(*out, *in) - } if in.StartTime != nil { in, out := &in.StartTime, &out.StartTime *out = (*in).DeepCopy() @@ -288,6 +254,16 @@ func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { in, out := &in.CompletionTime, &out.CompletionTime *out = (*in).DeepCopy() } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(int64) + **out = **in + } + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -297,90 +273,105 @@ func (in *AgentPlaybookRunStatus) DeepCopyInto(out *AgentPlaybookRunStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookRunStatus. -func (in *AgentPlaybookRunStatus) DeepCopy() *AgentPlaybookRunStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunStatus. +func (in *AgentRunStatus) DeepCopy() *AgentRunStatus { if in == nil { return nil } - out := new(AgentPlaybookRunStatus) + out := new(AgentRunStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookSpec) DeepCopyInto(out *AgentPlaybookSpec) { +func (in *AgentSkillCardRef) DeepCopyInto(out *AgentSkillCardRef) { *out = *in - if in.Stages != nil { - in, out := &in.Stages, &out.Stages - *out = make([]AgentPlaybookStage, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookSpec. -func (in *AgentPlaybookSpec) DeepCopy() *AgentPlaybookSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCardRef. +func (in *AgentSkillCardRef) DeepCopy() *AgentSkillCardRef { if in == nil { return nil } - out := new(AgentPlaybookSpec) + out := new(AgentSkillCardRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookStage) DeepCopyInto(out *AgentPlaybookStage) { +func (in *AgentSkillCollectionRef) DeepCopyInto(out *AgentSkillCollectionRef) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookStage. -func (in *AgentPlaybookStage) DeepCopy() *AgentPlaybookStage { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCollectionRef. +func (in *AgentSkillCollectionRef) DeepCopy() *AgentSkillCollectionRef { if in == nil { return nil } - out := new(AgentPlaybookStage) + out := new(AgentSkillCollectionRef) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentPlaybookStatus) DeepCopyInto(out *AgentPlaybookStatus) { +func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]AgentProviderRef, len(*in)) + copy(*out, *in) + } + if in.SkillCards != nil { + in, out := &in.SkillCards, &out.SkillCards + *out = make([]AgentSkillCardRef, len(*in)) + copy(*out, *in) + } + if in.SkillCollections != nil { + in, out := &in.SkillCollections, &out.SkillCollections + *out = make([]AgentSkillCollectionRef, len(*in)) + copy(*out, *in) + } + if in.Params != nil { + in, out := &in.Params, &out.Params + *out = make([]AgentParam, len(*in)) + copy(*out, *in) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPlaybookStatus. -func (in *AgentPlaybookStatus) DeepCopy() *AgentPlaybookStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. +func (in *AgentSpec) DeepCopy() *AgentSpec { if in == nil { return nil } - out := new(AgentPlaybookStatus) + out := new(AgentSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentProviderRef) DeepCopyInto(out *AgentProviderRef) { +func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentProviderRef. -func (in *AgentProviderRef) DeepCopy() *AgentProviderRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. +func (in *AgentStatus) DeepCopy() *AgentStatus { if in == nil { return nil } - out := new(AgentProviderRef) + out := new(AgentStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRun) DeepCopyInto(out *AgentRun) { +func (in *AgentWorkflow) DeepCopyInto(out *AgentWorkflow) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -388,18 +379,18 @@ func (in *AgentRun) DeepCopyInto(out *AgentRun) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRun. -func (in *AgentRun) DeepCopy() *AgentRun { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflow. +func (in *AgentWorkflow) DeepCopy() *AgentWorkflow { if in == nil { return nil } - out := new(AgentRun) + out := new(AgentWorkflow) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentRun) DeepCopyObject() runtime.Object { +func (in *AgentWorkflow) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -407,31 +398,31 @@ func (in *AgentRun) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunList) DeepCopyInto(out *AgentRunList) { +func (in *AgentWorkflowList) DeepCopyInto(out *AgentWorkflowList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentRun, len(*in)) + *out = make([]AgentWorkflow, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunList. -func (in *AgentRunList) DeepCopy() *AgentRunList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowList. +func (in *AgentWorkflowList) DeepCopy() *AgentWorkflowList { if in == nil { return nil } - out := new(AgentRunList) + out := new(AgentWorkflowList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AgentRunList) DeepCopyObject() runtime.Object { +func (in *AgentWorkflowList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -439,37 +430,66 @@ func (in *AgentRunList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunModelSelection) DeepCopyInto(out *AgentRunModelSelection) { +func (in *AgentWorkflowRun) DeepCopyInto(out *AgentWorkflowRun) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunModelSelection. -func (in *AgentRunModelSelection) DeepCopy() *AgentRunModelSelection { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRun. +func (in *AgentWorkflowRun) DeepCopy() *AgentWorkflowRun { if in == nil { return nil } - out := new(AgentRunModelSelection) + out := new(AgentWorkflowRun) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentWorkflowRun) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunParam) DeepCopyInto(out *AgentRunParam) { +func (in *AgentWorkflowRunList) DeepCopyInto(out *AgentWorkflowRunList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AgentWorkflowRun, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunParam. -func (in *AgentRunParam) DeepCopy() *AgentRunParam { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunList. +func (in *AgentWorkflowRunList) DeepCopy() *AgentWorkflowRunList { if in == nil { return nil } - out := new(AgentRunParam) + out := new(AgentWorkflowRunList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentWorkflowRunList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { +func (in *AgentWorkflowRunSpec) DeepCopyInto(out *AgentWorkflowRunSpec) { *out = *in if in.Models != nil { in, out := &in.Models, &out.Models @@ -497,19 +517,39 @@ func (in *AgentRunSpec) DeepCopyInto(out *AgentRunSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunSpec. -func (in *AgentRunSpec) DeepCopy() *AgentRunSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunSpec. +func (in *AgentWorkflowRunSpec) DeepCopy() *AgentWorkflowRunSpec { if in == nil { return nil } - out := new(AgentRunSpec) + out := new(AgentWorkflowRunSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { +func (in *AgentWorkflowRunStageStatus) DeepCopyInto(out *AgentWorkflowRunStageStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunStageStatus. +func (in *AgentWorkflowRunStageStatus) DeepCopy() *AgentWorkflowRunStageStatus { + if in == nil { + return nil + } + out := new(AgentWorkflowRunStageStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentWorkflowRunStatus) DeepCopyInto(out *AgentWorkflowRunStatus) { *out = *in + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make([]AgentWorkflowRunStageStatus, len(*in)) + copy(*out, *in) + } if in.StartTime != nil { in, out := &in.StartTime, &out.StartTime *out = (*in).DeepCopy() @@ -518,16 +558,6 @@ func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { in, out := &in.CompletionTime, &out.CompletionTime *out = (*in).DeepCopy() } - if in.Duration != nil { - in, out := &in.Duration, &out.Duration - *out = new(int64) - **out = **in - } - if in.SecretKeyRef != nil { - in, out := &in.SecretKeyRef, &out.SecretKeyRef - *out = new(corev1.LocalObjectReference) - **out = **in - } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -537,83 +567,53 @@ func (in *AgentRunStatus) DeepCopyInto(out *AgentRunStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRunStatus. -func (in *AgentRunStatus) DeepCopy() *AgentRunStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowRunStatus. +func (in *AgentWorkflowRunStatus) DeepCopy() *AgentWorkflowRunStatus { if in == nil { return nil } - out := new(AgentRunStatus) + out := new(AgentWorkflowRunStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSkillCardRef) DeepCopyInto(out *AgentSkillCardRef) { +func (in *AgentWorkflowSpec) DeepCopyInto(out *AgentWorkflowSpec) { *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCardRef. -func (in *AgentSkillCardRef) DeepCopy() *AgentSkillCardRef { - if in == nil { - return nil + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make([]AgentWorkflowStage, len(*in)) + copy(*out, *in) } - out := new(AgentSkillCardRef) - in.DeepCopyInto(out) - return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSkillCollectionRef) DeepCopyInto(out *AgentSkillCollectionRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSkillCollectionRef. -func (in *AgentSkillCollectionRef) DeepCopy() *AgentSkillCollectionRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowSpec. +func (in *AgentWorkflowSpec) DeepCopy() *AgentWorkflowSpec { if in == nil { return nil } - out := new(AgentSkillCollectionRef) + out := new(AgentWorkflowSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { +func (in *AgentWorkflowStage) DeepCopyInto(out *AgentWorkflowStage) { *out = *in - if in.Providers != nil { - in, out := &in.Providers, &out.Providers - *out = make([]AgentProviderRef, len(*in)) - copy(*out, *in) - } - if in.SkillCards != nil { - in, out := &in.SkillCards, &out.SkillCards - *out = make([]AgentSkillCardRef, len(*in)) - copy(*out, *in) - } - if in.SkillCollections != nil { - in, out := &in.SkillCollections, &out.SkillCollections - *out = make([]AgentSkillCollectionRef, len(*in)) - copy(*out, *in) - } - if in.Params != nil { - in, out := &in.Params, &out.Params - *out = make([]AgentParam, len(*in)) - copy(*out, *in) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. -func (in *AgentSpec) DeepCopy() *AgentSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowStage. +func (in *AgentWorkflowStage) DeepCopy() *AgentWorkflowStage { if in == nil { return nil } - out := new(AgentSpec) + out := new(AgentWorkflowStage) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { +func (in *AgentWorkflowStatus) DeepCopyInto(out *AgentWorkflowStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -624,12 +624,12 @@ func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentStatus. -func (in *AgentStatus) DeepCopy() *AgentStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentWorkflowStatus. +func (in *AgentWorkflowStatus) DeepCopy() *AgentWorkflowStatus { if in == nil { return nil } - out := new(AgentStatus) + out := new(AgentWorkflowStatus) in.DeepCopyInto(out) return out } diff --git a/changes/unreleased/agentplaybook-controllers.yaml b/changes/unreleased/agentplaybook-controllers.yaml deleted file mode 100644 index d70f709a..00000000 --- a/changes/unreleased/agentplaybook-controllers.yaml +++ /dev/null @@ -1,10 +0,0 @@ -kind: feature -description: > - Add AgentPlaybook and AgentPlaybookRun controllers for sequential - multi-stage agent orchestration. AgentPlaybook validates referenced - Agents are Ready. AgentPlaybookRun creates AgentRuns per stage - sequentially, forwarding params, models, env, and envFrom from - the playbook run to each stage. Includes deterministic AgentRun - naming, playbook guide passthrough via KONVEYOR_PLAYBOOK_INSTRUCTIONS, - and a fix for the Sandbox finished-reason constant (Succeeded → - PodSucceeded) to match Agent Sandbox v0.5.0. diff --git a/changes/unreleased/agentworkflow-controllers.yaml b/changes/unreleased/agentworkflow-controllers.yaml new file mode 100644 index 00000000..829cdba4 --- /dev/null +++ b/changes/unreleased/agentworkflow-controllers.yaml @@ -0,0 +1,10 @@ +kind: feature +description: > + Add AgentWorkflow and AgentWorkflowRun controllers for sequential + multi-stage agent orchestration. AgentWorkflow validates referenced + Agents are Ready. AgentWorkflowRun creates AgentRuns per stage + sequentially, forwarding params, models, env, and envFrom from + the workflow run to each stage. Includes deterministic AgentRun + naming, workflow guide passthrough via KONVEYOR_WORKFLOW_GUIDE, + and a fix for the Sandbox finished-reason constant (Succeeded → + PodSucceeded) to match Agent Sandbox v0.5.0. diff --git a/changes/unreleased/scaffold-crds.yaml b/changes/unreleased/scaffold-crds.yaml index cd0942f2..bd98b2da 100644 --- a/changes/unreleased/scaffold-crds.yaml +++ b/changes/unreleased/scaffold-crds.yaml @@ -1,5 +1,5 @@ kind: feature description: > - Defined Agent, AgentRun, AgentPlaybook, AgentPlaybookRun, SkillCard, + Defined Agent, AgentRun, AgentWorkflow, AgentWorkflowRun, SkillCard, SkillCollection, and LLMProvider CRDs with CEL validation, immutability rules, and printer columns. diff --git a/cmd/main.go b/cmd/main.go index 3c60e8d0..2c1cd495 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -215,18 +215,19 @@ func main() { setupLog.Error(err, "Failed to create controller", "controller", "AgentRun") os.Exit(1) } - if err := (&controller.AgentPlaybookReconciler{ + if err := (&controller.AgentWorkflowReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "Failed to create controller", "controller", "AgentPlaybook") + setupLog.Error(err, "Failed to create controller", "controller", "AgentWorkflow") os.Exit(1) } - if err := (&controller.AgentPlaybookRunReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + if err := (&controller.AgentWorkflowRunReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("agentworkflowrun-controller"), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "Failed to create controller", "controller", "AgentPlaybookRun") + setupLog.Error(err, "Failed to create controller", "controller", "AgentWorkflowRun") os.Exit(1) } // +kubebuilder:scaffold:builder diff --git a/config/crd/bases/konveyor.io_agentplaybookruns.yaml b/config/crd/bases/konveyor.io_agentworkflowruns.yaml similarity index 95% rename from config/crd/bases/konveyor.io_agentplaybookruns.yaml rename to config/crd/bases/konveyor.io_agentworkflowruns.yaml index 88ab7527..43f80b85 100644 --- a/config/crd/bases/konveyor.io_agentplaybookruns.yaml +++ b/config/crd/bases/konveyor.io_agentworkflowruns.yaml @@ -4,21 +4,21 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.21.0 - name: agentplaybookruns.konveyor.io + name: agentworkflowruns.konveyor.io spec: group: konveyor.io names: - kind: AgentPlaybookRun - listKind: AgentPlaybookRunList - plural: agentplaybookruns + kind: AgentWorkflowRun + listKind: AgentWorkflowRunList + plural: agentworkflowruns shortNames: - - apr - singular: agentplaybookrun + - awr + singular: agentworkflowrun scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.playbookRef - name: Playbook + - jsonPath: .spec.workflowRef + name: Workflow type: string - jsonPath: .status.phase name: Phase @@ -33,8 +33,8 @@ spec: schema: openAPIV3Schema: description: |- - AgentPlaybookRun is a request to execute an AgentPlaybook. It references - an AgentPlaybook and carries generic parameters. The controller orchestrates + AgentWorkflowRun is a request to execute an AgentWorkflow. It references + an AgentWorkflow and carries generic parameters. The controller orchestrates execution: creates an AgentRun per stage, manages cross-stage handoff via committed files on a shared target branch. properties: @@ -57,7 +57,7 @@ spec: type: object spec: description: |- - AgentPlaybookRunSpec defines the desired state of an AgentPlaybookRun. + AgentWorkflowRunSpec defines the desired state of an AgentWorkflowRun. The spec is immutable once created — delete and recreate to change values. properties: env: @@ -328,27 +328,27 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - playbookRef: - description: PlaybookRef is the name of the AgentPlaybook CR to execute. + workflowRef: + description: WorkflowRef is the name of the AgentWorkflow CR to execute. minLength: 1 type: string required: - - playbookRef + - workflowRef type: object x-kubernetes-validations: - message: spec is immutable rule: self == oldSelf status: - description: AgentPlaybookRunStatus defines the observed state of an AgentPlaybookRun. + description: AgentWorkflowRunStatus defines the observed state of an AgentWorkflowRun. properties: completionTime: - description: CompletionTime is the time the playbook run finished. + description: CompletionTime is the time the workflow run finished. format: date-time type: string conditions: description: |- Conditions represent the latest available observations of the - AgentPlaybookRun's state. + AgentWorkflowRun's state. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -417,7 +417,7 @@ spec: type: integer phase: default: Pending - description: Phase is the current phase of the overall playbook run. + description: Phase is the current phase of the overall workflow run. enum: - Pending - Running @@ -428,8 +428,8 @@ spec: description: Stages tracks the status of each stage. items: description: |- - AgentPlaybookRunStageStatus tracks the status of a single stage within - a playbook run. + AgentWorkflowRunStageStatus tracks the status of a single stage within + a workflow run. properties: agentRunName: description: AgentRunName is the name of the AgentRun CR created @@ -437,7 +437,7 @@ spec: type: string name: description: Name is the stage name, matching a stage in the - AgentPlaybook. + AgentWorkflow. type: string phase: description: Phase is the current phase of this stage. @@ -456,7 +456,7 @@ spec: - name x-kubernetes-list-type: map startTime: - description: StartTime is the time the playbook run started. + description: StartTime is the time the workflow run started. format: date-time type: string type: object diff --git a/config/crd/bases/konveyor.io_agentplaybooks.yaml b/config/crd/bases/konveyor.io_agentworkflows.yaml similarity index 92% rename from config/crd/bases/konveyor.io_agentplaybooks.yaml rename to config/crd/bases/konveyor.io_agentworkflows.yaml index 3a2295bc..fa625db2 100644 --- a/config/crd/bases/konveyor.io_agentplaybooks.yaml +++ b/config/crd/bases/konveyor.io_agentworkflows.yaml @@ -4,16 +4,16 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.21.0 - name: agentplaybooks.konveyor.io + name: agentworkflows.konveyor.io spec: group: konveyor.io names: - kind: AgentPlaybook - listKind: AgentPlaybookList - plural: agentplaybooks + kind: AgentWorkflow + listKind: AgentWorkflowList + plural: agentworkflows shortNames: - - ap - singular: agentplaybook + - aw + singular: agentworkflow scope: Namespaced versions: - additionalPrinterColumns: @@ -27,9 +27,9 @@ spec: schema: openAPIV3Schema: description: |- - AgentPlaybook is a reusable playbook combining a high-level guide with an + AgentWorkflow is a reusable workflow combining a high-level guide with an ordered sequence of stages. Each stage references an Agent and carries - instructions. An AgentPlaybook is a template — creating one does not + instructions. An AgentWorkflow is a template — creating one does not execute anything. properties: apiVersion: @@ -50,7 +50,7 @@ spec: metadata: type: object spec: - description: AgentPlaybookSpec defines the desired state of an AgentPlaybook. + description: AgentWorkflowSpec defines the desired state of an AgentWorkflow. properties: guide: description: |- @@ -66,7 +66,7 @@ spec: handoff files. items: description: |- - AgentPlaybookStage defines one stage in a playbook. + AgentWorkflowStage defines one stage in a workflow. Each stage references an Agent and carries instructions. properties: agentRef: @@ -81,7 +81,7 @@ spec: type: string name: description: |- - Name is the stage name, unique within the playbook. + Name is the stage name, unique within the workflow. Must be a valid Kubernetes label value (lowercase alphanumeric, hyphens, dots, max 63 chars) since it is used in labels on child AgentRun resources. @@ -102,12 +102,12 @@ spec: - stages type: object status: - description: AgentPlaybookStatus defines the observed state of an AgentPlaybook. + description: AgentWorkflowStatus defines the observed state of an AgentWorkflow. properties: conditions: description: |- Conditions represent the latest available observations of the - AgentPlaybook's state. + AgentWorkflow's state. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5975c64e..eb601fbc 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,6 @@ resources: - bases/konveyor.io_llmproviders.yaml - bases/konveyor.io_agents.yaml - bases/konveyor.io_agentruns.yaml -- bases/konveyor.io_agentplaybooks.yaml -- bases/konveyor.io_agentplaybookruns.yaml +- bases/konveyor.io_agentworkflows.yaml +- bases/konveyor.io_agentworkflowruns.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3d4cc4eb..6046415c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,13 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - "" resources: @@ -38,10 +45,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns - - agentplaybooks - agentruns - agents + - agentworkflowruns + - agentworkflows - llmproviders - skillcards - skillcollections @@ -56,10 +63,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns/finalizers - - agentplaybooks/finalizers - agentruns/finalizers - agents/finalizers + - agentworkflowruns/finalizers + - agentworkflows/finalizers - llmproviders/finalizers - skillcards/finalizers - skillcollections/finalizers @@ -68,10 +75,10 @@ rules: - apiGroups: - konveyor.io resources: - - agentplaybookruns/status - - agentplaybooks/status - agentruns/status - agents/status + - agentworkflowruns/status + - agentworkflows/status - llmproviders/status - skillcards/status - skillcollections/status diff --git a/docs/adr/0001-agentic-platform-crd-architecture.md b/docs/adr/0001-agentic-platform-crd-architecture.md index ba585e0e..c1828702 100644 --- a/docs/adr/0001-agentic-platform-crd-architecture.md +++ b/docs/adr/0001-agentic-platform-crd-architecture.md @@ -48,14 +48,14 @@ Kubernetes API with the emerging OCI skills ecosystem. | **SkillCollection** | Group of skills. Each entry references a skill by OCI image ref, git source, or SkillCard CR name. | | **LLMProvider** | LLM service endpoint, credentials (Secret ref), and available models with context window sizes and optional tier labels. | | **Agent** | Template declaring what is available for execution. References one or more LLMProviders, SkillCards, SkillCollections, a container image, a prompt, and declares typed parameters (inputs the AgentRun must supply). Does not select a specific model — model selection happens at execution time. Analogous to a Tekton Task. | -| **AgentPlaybook** | Ordered sequence of stages. Each stage references an Agent and carries instructions. Stages execute sequentially with fresh agent sessions. Cross-stage continuity is through git branch content. | +| **AgentWorkflow** | Ordered sequence of stages. Each stage references an Agent and carries instructions. Stages execute sequentially with fresh agent sessions. Cross-stage continuity is through git branch content. | **Execution resources** (created to trigger work): | CRD | Purpose | |-----|---------| | **AgentRun** | Execute a single Agent with specific values. References an Agent, selects models, supplies parameter values, carries instructions, and may include additional `env` and `envFrom` entries passed through to the Sandbox. The controller validates params against the Agent's declarations, creates a Sandbox, and tracks status. Analogous to a Tekton TaskRun. | -| **AgentPlaybookRun** | Execute an AgentPlaybook. Creates AgentRun CRs sequentially per stage, all sharing the same target branch. Each stage reads the previous stage's committed handoff files. | +| **AgentWorkflowRun** | Execute an AgentWorkflow. Creates AgentRun CRs sequentially per stage, all sharing the same target branch. Each stage reads the previous stage's committed handoff files. | ### Naming alignment with skillimage @@ -211,7 +211,7 @@ isolation requires OpenShell (NVIDIA) filesystem policy enforcement, which can deny reads to the Secret mount path at the kernel level. The harness manages all credentialed git operations. -No PVCs survive between runs. Cross-stage continuity in playbooks +No PVCs survive between runs. Cross-stage continuity in workflows is through committed files on the shared branch. Parallel agents on the same application use different branches. @@ -228,10 +228,10 @@ repo on a `konveyor/` branch. This follows the pattern established by the assets-generation enhancement and implemented in tackle2-addon-platform. -### AgentPlaybook execution model +### AgentWorkflow execution model -An AgentPlaybook is a flat sequence of stages. Each stage references -an Agent and carries instructions. The AgentPlaybookRun controller +An AgentWorkflow is a flat sequence of stages. Each stage references +an Agent and carries instructions. The AgentWorkflowRun controller creates AgentRuns sequentially, all targeting the same branch. Each stage gets a fresh agent session. Cross-stage knowledge @@ -240,7 +240,7 @@ transfer happens through committed files on the branch: 1. **Handoff files** — each stage commits `.konveyor/handoff.md` summarizing what was accomplished and what remains. The next stage's agent reads this on checkout. -2. **Playbook guide** — the playbook's guide is committed as +2. **Workflow guide** — the workflow's guide is committed as `.konveyor/guide.md` before stage 1, providing ambient context. Prompt composition at execution time: @@ -269,7 +269,7 @@ executions of that Agent, enabling organizational learning. A POC exists in dymurray/tackle2-addon-kai using mempalace. Memory service integration is deferred to Phase 4 — after the core execution -model (AgentRun), playbooks (AgentPlaybook), and configurable git +model (AgentRun), workflows (AgentWorkflow), and configurable git strategy are proven. ### Hub as a data service @@ -450,16 +450,16 @@ otherwise need to build ourselves. ### Tekton as the orchestration layer Tekton Tasks/Pipelines were considered for orchestrating multi-stage -agent playbooks. Deferred, not rejected: the MVP requires only +agent workflows. Deferred, not rejected: the MVP requires only sequential stage execution, and a custom controller is simpler than adding a Tekton dependency. The architecture does not preclude Tekton -integration later — AgentPlaybookRun could generate Tekton +integration later — AgentWorkflowRun could generate Tekton PipelineRuns as an implementation detail when users need conditionals, parallelism, retries, or supply chain signing. ### Stages with phases and session continuity -We originally designed AgentPlaybook with stages containing multiple +We originally designed AgentWorkflow with stages containing multiple phases, where phases within a stage shared session state via a PVC (SQLite database). This provided full conversation continuity within a stage. diff --git a/docs/adr/0003-hub-curated-api-for-agent-resources.md b/docs/adr/0003-hub-curated-api-for-agent-resources.md index 720866e2..db2bae72 100644 --- a/docs/adr/0003-hub-curated-api-for-agent-resources.md +++ b/docs/adr/0003-hub-curated-api-for-agent-resources.md @@ -8,7 +8,7 @@ The agentic platform controller introduces seven CRDs under the `konveyor.io` API group (SkillCard, SkillCollection, LLMProvider, -Agent, AgentRun, AgentPlaybook, AgentPlaybookRun). The UI needs to +Agent, AgentRun, AgentWorkflow, AgentWorkflowRun). The UI needs to create, read, update, and delete these resources. The question is how the UI accesses them. diff --git a/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md b/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md index b373efb8..270af814 100644 --- a/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md +++ b/docs/adr/0007-harness-thin-runner-and-skillcard-skills.md @@ -12,7 +12,7 @@ Each stage was a Go package that constructed multi-turn ACP prompts from YAML recipe files and an embedded skill bundle. The harness owned both stage sequencing and migration intelligence. -With the AgentPlaybookRun controller (ADR 0001) handling stage +With the AgentWorkflowRun controller (ADR 0001) handling stage sequencing, the harness no longer needs orchestration logic. Meanwhile, the SkillCard CRD and OCI-based skill packaging provide a clean mechanism for delivering migration knowledge to agent pods at runtime. @@ -45,7 +45,7 @@ patterns to apply. Its responsibilities are: 8. Discover skills from `/opt/skills/*/SKILL.md` (glob) 9. Build a single prompt from four context layers: - `KONVEYOR_PROMPT` — agent-level standing instructions - - `KONVEYOR_PLAYBOOK_INSTRUCTIONS` — playbook guide context + - `KONVEYOR_WORKFLOW_GUIDE` — workflow guide context - Skill content (concatenated from all discovered skills) - `KONVEYOR_INSTRUCTIONS` — stage-specific task 10. Start a filesystem watcher for incremental push @@ -289,7 +289,7 @@ reconciler that watches pod exit codes. - **Test harness scaffolding.** `hack/harness-test/setup.sh` builds skill OCI images locally, loads them into Kind, and applies - SkillCard + Agent + AgentPlaybook + AgentPlaybookRun CRs for + SkillCard + Agent + AgentWorkflow + AgentWorkflowRun CRs for end-to-end testing. diff --git a/docs/slides/demo-2026-07-16.md b/docs/slides/demo-2026-07-16.md index 905e146e..8af5ed4a 100644 --- a/docs/slides/demo-2026-07-16.md +++ b/docs/slides/demo-2026-07-16.md @@ -51,7 +51,7 @@ A **Kubernetes controller** for managing AI agent workloads in Konveyor | **SkillCollection** | Group of related skills | | **LLMProvider** | LLM endpoint, credentials, model catalog | | **Agent** | Template: image, providers, skills, prompt, parameters | -| **AgentPlaybook** | Ordered sequence of stages, each referencing an Agent | +| **AgentWorkflow** | Ordered sequence of stages, each referencing an Agent | --- @@ -62,7 +62,7 @@ A **Kubernetes controller** for managing AI agent workloads in Konveyor | CRD | Purpose | |---|---| | **AgentRun** | Concrete invocation of an Agent with specific values | -| **AgentPlaybookRun** | Runs a playbook — creates sequential AgentRuns | +| **AgentWorkflowRun** | Runs a workflow — creates sequential AgentRuns | **Agent vs AgentRun** — the key distinction: - **Agent** = what's available (template) @@ -87,8 +87,8 @@ Parameters are **opaque** — the controller injects them but never interprets t Hub exposes **purpose-built REST endpoints** for all agent resources: ``` -/hub/agents /hub/agentplaybooks -/hub/agentruns /hub/agentplaybookruns +/hub/agents /hub/agentworkflows +/hub/agentruns /hub/agentworkflowruns /hub/skillcards /hub/skillcollections /hub/llmproviders ``` @@ -150,9 +150,9 @@ Artifacts: session state, handoff markdown, verification reports, execution logs --- -# Multi-Stage Playbooks +# Multi-Stage Workflows -**AgentPlaybook** defines ordered stages, each with its own Agent: +**AgentWorkflow** defines ordered stages, each with its own Agent: | Stage | Agent | What it does | |---|---|---| @@ -160,7 +160,7 @@ Artifacts: session state, handoff markdown, verification reports, execution logs | **migrate** | code-migrator | Execute the migration plan | | **verify** | test-runner | Run tests and verify correctness | -Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a shared branch. The playbook guide is injected as `KONVEYOR_PLAYBOOK_INSTRUCTIONS`. +Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a shared branch. The workflow guide is injected as `KONVEYOR_WORKFLOW_GUIDE`. --- @@ -182,7 +182,7 @@ Each stage runs in a **fresh Sandbox**, sharing work via **git commits** on a sh # What's Next -**Landing now:** PRs #33 (harness), #35 (client stack), #36 (playbook controllers) +**Landing now:** PRs #33 (harness), #35 (client stack), #36 (workflow controllers) **Roadmap:** diff --git a/hack/harness-test/resources.yaml b/hack/harness-test/resources.yaml index 2fd3dbb7..12cf9818 100644 --- a/hack/harness-test/resources.yaml +++ b/hack/harness-test/resources.yaml @@ -1,5 +1,5 @@ # Shared resources for harness integration tests in Kind. -# Creates: LLMProvider (used by playbook-resources.yaml) +# Creates: LLMProvider (used by workflow-resources.yaml) # # Usage: # hack/harness-test/setup.sh diff --git a/hack/harness-test/playbook-resources.yaml b/hack/harness-test/workflow-resources.yaml similarity index 94% rename from hack/harness-test/playbook-resources.yaml rename to hack/harness-test/workflow-resources.yaml index 08594126..8824681a 100644 --- a/hack/harness-test/playbook-resources.yaml +++ b/hack/harness-test/workflow-resources.yaml @@ -1,4 +1,4 @@ -# Example resources for testing the AgentPlaybook + AgentPlaybookRun flow. +# Example resources for testing the AgentWorkflow + AgentWorkflowRun flow. # Migrates the coolstore Java EE app to Quarkus using 3 stages. # # Prerequisites: @@ -7,7 +7,7 @@ # # Usage: # kubectl apply -f hack/harness-test/resources.yaml -# kubectl apply -f hack/harness-test/playbook-resources.yaml +# kubectl apply -f hack/harness-test/workflow-resources.yaml --- apiVersion: konveyor.io/v1alpha1 @@ -130,7 +130,7 @@ spec: --- apiVersion: konveyor.io/v1alpha1 -kind: AgentPlaybook +kind: AgentWorkflow metadata: name: java-ee-to-quarkus spec: @@ -148,11 +148,11 @@ spec: --- apiVersion: konveyor.io/v1alpha1 -kind: AgentPlaybookRun +kind: AgentWorkflowRun metadata: name: coolstore-migration-__TIMESTAMP__ spec: - playbookRef: java-ee-to-quarkus + workflowRef: java-ee-to-quarkus models: - role: primary provider: gcp-vertex-ai diff --git a/harness/README.md b/harness/README.md index f656ef80..0c3dcc4e 100644 --- a/harness/README.md +++ b/harness/README.md @@ -23,7 +23,7 @@ └──────────────────────────────────────────────────────┘ ``` -The harness sends **one prompt** per stage. The AgentPlaybookRun controller handles stage sequencing — the harness is identical in every stage image. +The harness sends **one prompt** per stage. The AgentWorkflowRun controller handles stage sequencing — the harness is identical in every stage image. --- @@ -70,7 +70,7 @@ All configuration is via environment variables — there is no config file or `i | `HARNESS_WORK_DIR` | `/workspace/repo` | Clone directory | | `HARNESS_SKILLS_DIR` | `/opt/skills` | Skills mount directory | | `KONVEYOR_PROMPT` | — | Agent-level standing instructions | -| `KONVEYOR_PLAYBOOK_INSTRUCTIONS` | — | Playbook guide context | +| `KONVEYOR_WORKFLOW_GUIDE` | — | Workflow guide context | | `KONVEYOR_INSTRUCTIONS` | — | Stage-specific task instructions | --- diff --git a/harness/internal/config/config.go b/harness/internal/config/config.go index 4d4a9c5d..ac9dc1c4 100644 --- a/harness/internal/config/config.go +++ b/harness/internal/config/config.go @@ -71,9 +71,9 @@ func LoadFromEnv() (*Config, error) { // workflowGuideFromEnv reads the workflow guide the controller injects. // -// konveyor/agentic-controller#80 renames KONVEYOR_PLAYBOOK_INSTRUCTIONS to -// KONVEYOR_WORKFLOW_GUIDE. Reading both means the harness works either side of -// that merge; drop the fallback once #80 has landed everywhere. +// The canonical env var is KONVEYOR_WORKFLOW_GUIDE (set by the controller). +// KONVEYOR_PLAYBOOK_INSTRUCTIONS is the legacy name; drop the fallback +// once all deployed controllers use the new name. func workflowGuideFromEnv() string { if v := os.Getenv("KONVEYOR_WORKFLOW_GUIDE"); v != "" { return v diff --git a/harness/internal/config/config_test.go b/harness/internal/config/config_test.go index 72bd3b91..34e78e45 100644 --- a/harness/internal/config/config_test.go +++ b/harness/internal/config/config_test.go @@ -158,8 +158,8 @@ func TestLoadFromEnvReadsPromptLayers(t *testing.T) { } } -// #80 renames the env var; the harness reads either so merge order does not -// matter. Remove with the fallback once #80 has landed everywhere. +// The legacy env var KONVEYOR_PLAYBOOK_INSTRUCTIONS is still read as a +// fallback. Remove this test when the fallback is dropped. func TestLoadFromEnvFallsBackToPlaybookInstructions(t *testing.T) { clearKonveyorEnv(t) setRequiredEnv(t) diff --git a/internal/controller/agentrun_controller.go b/internal/controller/agentrun_controller.go index c9139340..9c884e7c 100644 --- a/internal/controller/agentrun_controller.go +++ b/internal/controller/agentrun_controller.go @@ -368,7 +368,13 @@ func (r *AgentRunReconciler) createSandbox( }, }, Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, + // Never restart — a failed container must reach a terminal + // phase so the AgentRun (and workflow stage) can observe + // the failure. OnFailure would cause infinite crashloops + // (#51). The tradeoff is that transient failures (image + // pull blips, node eviction) are not retried. Bounded + // retry (backoffLimit-style) can be added later if needed. + RestartPolicy: corev1.RestartPolicyNever, Containers: []corev1.Container{ { Name: "agent", diff --git a/internal/controller/agentrun_controller_test.go b/internal/controller/agentrun_controller_test.go index 5b682de6..f5070fa6 100644 --- a/internal/controller/agentrun_controller_test.go +++ b/internal/controller/agentrun_controller_test.go @@ -352,6 +352,9 @@ var _ = Describe("AgentRun Controller", func() { Expect(sandbox.Spec.PodTemplate.ObjectMeta.Labels).To(HaveKeyWithValue("konveyor.io/agentrun", name)) Expect(sandbox.Spec.PodTemplate.ObjectMeta.Labels).To(HaveKeyWithValue("konveyor.io/agent", agentName)) + By("verifying restartPolicy is Never so failed stages are observable (#51)") + Expect(sandbox.Spec.PodTemplate.Spec.RestartPolicy).To(Equal(corev1.RestartPolicyNever)) + By("verifying the single-key provider credential is injected as API_KEY") container := sandbox.Spec.PodTemplate.Spec.Containers[0] var apiKey *corev1.EnvVar diff --git a/internal/controller/agentplaybook_controller.go b/internal/controller/agentworkflow_controller.go similarity index 60% rename from internal/controller/agentplaybook_controller.go rename to internal/controller/agentworkflow_controller.go index fc9bc29c..c20260ce 100644 --- a/internal/controller/agentplaybook_controller.go +++ b/internal/controller/agentworkflow_controller.go @@ -36,44 +36,44 @@ import ( ) const ( - // playbookAgentRefIndexField is the field index for looking up - // AgentPlaybooks by their stages' agentRef values. - playbookAgentRefIndexField = ".spec.stages.agentRef" + // workflowAgentRefIndexField is the field index for looking up + // AgentWorkflows by their stages' agentRef values. + workflowAgentRefIndexField = ".spec.stages.agentRef" ) -// AgentPlaybookReconciler reconciles an AgentPlaybook object. -type AgentPlaybookReconciler struct { +// AgentWorkflowReconciler reconciles an AgentWorkflow object. +type AgentWorkflowReconciler struct { client.Client Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks/finalizers,verbs=update +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows/finalizers,verbs=update -// Reconcile handles AgentPlaybook reconciliation. +// Reconcile handles AgentWorkflow reconciliation. // // The controller validates that all Agents referenced by stages exist and -// are Ready, then reports aggregate readiness on the AgentPlaybook. -func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// are Ready, then reports aggregate readiness on the AgentWorkflow. +func (r *AgentWorkflowReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - var playbook konveyoriov1alpha1.AgentPlaybook - if err := r.Get(ctx, req.NamespacedName, &playbook); err != nil { + var workflow konveyoriov1alpha1.AgentWorkflow + if err := r.Get(ctx, req.NamespacedName, &workflow); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - logger.V(1).Info("Reconciling AgentPlaybook", "name", playbook.Name) + logger.V(1).Info("Reconciling AgentWorkflow", "name", workflow.Name) - original := playbook.DeepCopy() - playbook.Status.ObservedGeneration = playbook.Generation + original := workflow.DeepCopy() + workflow.Status.ObservedGeneration = workflow.Generation var notReadyReasons []string // Validate that each stage's Agent exists and is Ready. - for _, stage := range playbook.Spec.Stages { + for _, stage := range workflow.Spec.Stages { var agent konveyoriov1alpha1.Agent - agentKey := types.NamespacedName{Namespace: playbook.Namespace, Name: stage.AgentRef} + agentKey := types.NamespacedName{Namespace: workflow.Namespace, Name: stage.AgentRef} if err := r.Get(ctx, agentKey, &agent); err != nil { if errors.IsNotFound(err) { notReadyReasons = append(notReadyReasons, @@ -91,25 +91,25 @@ func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Reques } if len(notReadyReasons) == 0 { - meta.SetStatusCondition(&playbook.Status.Conditions, metav1.Condition{ + meta.SetStatusCondition(&workflow.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionTrue, - ObservedGeneration: playbook.Generation, + ObservedGeneration: workflow.Generation, Reason: "AllAgentsReady", Message: "All stage Agents are ready", }) } else { - meta.SetStatusCondition(&playbook.Status.Conditions, metav1.Condition{ + meta.SetStatusCondition(&workflow.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionFalse, - ObservedGeneration: playbook.Generation, + ObservedGeneration: workflow.Generation, Reason: "AgentsNotReady", Message: strings.Join(notReadyReasons, "; "), }) } - if err := r.Status().Patch(ctx, &playbook, client.MergeFrom(original)); err != nil { - logger.Error(err, "Failed to patch AgentPlaybook status") + if err := r.Status().Patch(ctx, &workflow, client.MergeFrom(original)); err != nil { + logger.Error(err, "Failed to patch AgentWorkflow status") return ctrl.Result{}, err } @@ -117,38 +117,38 @@ func (r *AgentPlaybookReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // SetupWithManager sets up the controller with the Manager. -func (r *AgentPlaybookReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Index AgentPlaybooks by their stages' agentRef values for +func (r *AgentWorkflowReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Index AgentWorkflows by their stages' agentRef values for // efficient reverse lookup when an Agent changes. if err := mgr.GetFieldIndexer().IndexField( context.Background(), - &konveyoriov1alpha1.AgentPlaybook{}, - playbookAgentRefIndexField, + &konveyoriov1alpha1.AgentWorkflow{}, + workflowAgentRefIndexField, func(obj client.Object) []string { - playbook := obj.(*konveyoriov1alpha1.AgentPlaybook) - refs := make([]string, len(playbook.Spec.Stages)) - for i, stage := range playbook.Spec.Stages { + workflow := obj.(*konveyoriov1alpha1.AgentWorkflow) + refs := make([]string, len(workflow.Spec.Stages)) + for i, stage := range workflow.Spec.Stages { refs[i] = stage.AgentRef } return refs }, ); err != nil { - return fmt.Errorf("indexing %s: %w", playbookAgentRefIndexField, err) + return fmt.Errorf("indexing %s: %w", workflowAgentRefIndexField, err) } return ctrl.NewControllerManagedBy(mgr). - For(&konveyoriov1alpha1.AgentPlaybook{}). + For(&konveyoriov1alpha1.AgentWorkflow{}). Watches( &konveyoriov1alpha1.Agent{}, - handler.EnqueueRequestsFromMapFunc(r.findPlaybooksForAgent), + handler.EnqueueRequestsFromMapFunc(r.findWorkflowsForAgent), ). - Named("agentplaybook"). + Named("agentworkflow"). Complete(r) } -// findPlaybooksForAgent returns reconcile requests for all AgentPlaybooks +// findWorkflowsForAgent returns reconcile requests for all AgentWorkflows // that reference the given Agent in any stage. -func (r *AgentPlaybookReconciler) findPlaybooksForAgent( +func (r *AgentWorkflowReconciler) findWorkflowsForAgent( ctx context.Context, obj client.Object, ) []reconcile.Request { @@ -157,18 +157,18 @@ func (r *AgentPlaybookReconciler) findPlaybooksForAgent( return nil } - var playbookList konveyoriov1alpha1.AgentPlaybookList - if err := r.List(ctx, &playbookList, + var workflowList konveyoriov1alpha1.AgentWorkflowList + if err := r.List(ctx, &workflowList, client.InNamespace(agent.Namespace), - client.MatchingFields{playbookAgentRefIndexField: agent.Name}, + client.MatchingFields{workflowAgentRefIndexField: agent.Name}, ); err != nil { - log.FromContext(ctx).Error(err, "Failed to list AgentPlaybooks for Agent", + log.FromContext(ctx).Error(err, "Failed to list AgentWorkflows for Agent", "agent", agent.Name) return nil } - requests := make([]reconcile.Request, len(playbookList.Items)) - for i, pb := range playbookList.Items { + requests := make([]reconcile.Request, len(workflowList.Items)) + for i, pb := range workflowList.Items { requests[i] = reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: pb.Namespace, diff --git a/internal/controller/agentplaybook_controller_test.go b/internal/controller/agentworkflow_controller_test.go similarity index 76% rename from internal/controller/agentplaybook_controller_test.go rename to internal/controller/agentworkflow_controller_test.go index 95e2a8ff..65ca2990 100644 --- a/internal/controller/agentplaybook_controller_test.go +++ b/internal/controller/agentworkflow_controller_test.go @@ -29,7 +29,7 @@ import ( konveyoriov1alpha1 "github.com/konveyor/agentic-controller/api/v1alpha1" ) -var _ = Describe("AgentPlaybook Controller", func() { +var _ = Describe("AgentWorkflow Controller", func() { const ( timeout = 10 * time.Second interval = 250 * time.Millisecond @@ -37,23 +37,23 @@ var _ = Describe("AgentPlaybook Controller", func() { Context("when a stage references a non-existent Agent", func() { const ( - playbookName = "ap-ctrl-missing-agent" + workflowName = "ap-ctrl-missing-agent" ) It("should set Ready=False with AgentsNotReady", func() { - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "plan", AgentRef: "nonexistent-agent"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -62,13 +62,13 @@ var _ = Describe("AgentPlaybook Controller", func() { g.Expect(readyCond.Message).To(ContainSubstring("nonexistent-agent")) }, timeout, interval).Should(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) }) }) Context("when all stage Agents exist and are Ready", func() { const ( - playbookName = "ap-ctrl-all-ready" + workflowName = "ap-ctrl-all-ready" agentName1 = "ap-ctrl-agent-1" agentName2 = "ap-ctrl-agent-2" provName = "ap-prov-ready" @@ -99,21 +99,21 @@ var _ = Describe("AgentPlaybook Controller", func() { Expect(k8sClient.Create(ctx, agent2)).To(Succeed()) waitForAgentReady(agentName2) - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Guide: "Test migration playbook", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Guide: "Test migration workflow", + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "plan", AgentRef: agentName1, Instructions: "Create a plan"}, {Name: "execute", AgentRef: agentName2, Instructions: "Execute the plan"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -121,7 +121,7 @@ var _ = Describe("AgentPlaybook Controller", func() { g.Expect(readyCond.Reason).To(Equal("AllAgentsReady")) }, timeout, interval).Should(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent1)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent2)).To(Succeed()) }) diff --git a/internal/controller/agentplaybookrun_controller.go b/internal/controller/agentworkflowrun_controller.go similarity index 63% rename from internal/controller/agentplaybookrun_controller.go rename to internal/controller/agentworkflowrun_controller.go index b5ece91b..780f072b 100644 --- a/internal/controller/agentplaybookrun_controller.go +++ b/internal/controller/agentworkflowrun_controller.go @@ -20,12 +20,15 @@ import ( "context" "fmt" + "strings" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/events" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -36,40 +39,42 @@ import ( ) const ( - // playbookRunRefIndexField is the field index for looking up - // AgentPlaybookRuns by playbookRef. - playbookRunRefIndexField = ".spec.playbookRef" + // workflowRunRefIndexField is the field index for looking up + // AgentWorkflowRuns by workflowRef. + workflowRunRefIndexField = ".spec.workflowRef" ) -// AgentPlaybookRunReconciler reconciles an AgentPlaybookRun object. -type AgentPlaybookRunReconciler struct { +// AgentWorkflowRunReconciler reconciles an AgentWorkflowRun object. +type AgentWorkflowRunReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Recorder events.EventRecorder } -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybookruns/finalizers,verbs=update -// +kubebuilder:rbac:groups=konveyor.io,resources=agentplaybooks,verbs=get;list;watch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflowruns/finalizers,verbs=update +// +kubebuilder:rbac:groups=konveyor.io,resources=agentworkflows,verbs=get;list;watch // +kubebuilder:rbac:groups=konveyor.io,resources=agentruns,verbs=get;list;watch;create +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch -// Reconcile handles AgentPlaybookRun reconciliation. +// Reconcile handles AgentWorkflowRun reconciliation. // -// The controller orchestrates sequential execution of playbook stages: -// 1. Looks up the referenced AgentPlaybook +// The controller orchestrates sequential execution of workflow stages: +// 1. Looks up the referenced AgentWorkflow // 2. Determines the current stage from status // 3. Creates an AgentRun for the current stage if none exists // 4. Watches the AgentRun to completion -// 5. Advances to the next stage or marks the playbook run as complete -func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// 5. Advances to the next stage or marks the workflow run as complete +func (r *AgentWorkflowRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - var pbRun konveyoriov1alpha1.AgentPlaybookRun + var pbRun konveyoriov1alpha1.AgentWorkflowRun if err := r.Get(ctx, req.NamespacedName, &pbRun); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - logger.V(1).Info("Reconciling AgentPlaybookRun", "name", pbRun.Name) + logger.V(1).Info("Reconciling AgentWorkflowRun", "name", pbRun.Name) original := pbRun.DeepCopy() pbRun.Status.ObservedGeneration = pbRun.Generation @@ -80,10 +85,10 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } - // Look up the referenced AgentPlaybook. - var playbook konveyoriov1alpha1.AgentPlaybook - playbookKey := types.NamespacedName{Namespace: pbRun.Namespace, Name: pbRun.Spec.PlaybookRef} - if err := r.Get(ctx, playbookKey, &playbook); err != nil { + // Look up the referenced AgentWorkflow. + var workflow konveyoriov1alpha1.AgentWorkflow + workflowKey := types.NamespacedName{Namespace: pbRun.Namespace, Name: pbRun.Spec.WorkflowRef} + if err := r.Get(ctx, workflowKey, &workflow); err != nil { if errors.IsNotFound(err) { pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() @@ -92,23 +97,23 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req Type: ConditionTypeReady, Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, - Reason: "PlaybookNotFound", - Message: fmt.Sprintf("AgentPlaybook %q not found", pbRun.Spec.PlaybookRef), + Reason: "WorkflowNotFound", + Message: fmt.Sprintf("AgentWorkflow %q not found", pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } return ctrl.Result{}, err } - // Check that the playbook is Ready. - playbookReady := meta.FindStatusCondition(playbook.Status.Conditions, ConditionTypeReady) - if playbookReady == nil || playbookReady.Status != metav1.ConditionTrue { + // Check that the workflow is Ready. + workflowReady := meta.FindStatusCondition(workflow.Status.Conditions, ConditionTypeReady) + if workflowReady == nil || workflowReady.Status != metav1.ConditionTrue { meta.SetStatusCondition(&pbRun.Status.Conditions, metav1.Condition{ Type: ConditionTypeReady, Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, - Reason: "PlaybookNotReady", - Message: fmt.Sprintf("AgentPlaybook %q is not Ready", pbRun.Spec.PlaybookRef), + Reason: "WorkflowNotReady", + Message: fmt.Sprintf("AgentWorkflow %q is not Ready", pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } @@ -122,9 +127,9 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // Initialize stage statuses if empty. if len(pbRun.Status.Stages) == 0 { - pbRun.Status.Stages = make([]konveyoriov1alpha1.AgentPlaybookRunStageStatus, len(playbook.Spec.Stages)) - for i, stage := range playbook.Spec.Stages { - pbRun.Status.Stages[i] = konveyoriov1alpha1.AgentPlaybookRunStageStatus{ + pbRun.Status.Stages = make([]konveyoriov1alpha1.AgentWorkflowRunStageStatus, len(workflow.Spec.Stages)) + for i, stage := range workflow.Spec.Stages { + pbRun.Status.Stages[i] = konveyoriov1alpha1.AgentWorkflowRunStageStatus{ Name: stage.Name, Phase: konveyoriov1alpha1.AgentRunPhasePending, } @@ -132,7 +137,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req } // Find the current stage to process. Use the snapshotted status - // stages as the source of truth — the playbook could have been + // stages as the source of truth — the workflow could have been // modified since the run started, but the run executes the stages // that were captured at initialization time. stageIndex := r.findCurrentStageIndex(&pbRun) @@ -152,18 +157,18 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return r.patchRunStatus(ctx, &pbRun, original) } - // Look up the stage definition from the playbook by name + // Look up the stage definition from the workflow by name // (matching the snapshotted status entry). stageStatus := &pbRun.Status.Stages[stageIndex] - var stage *konveyoriov1alpha1.AgentPlaybookStage - for i := range playbook.Spec.Stages { - if playbook.Spec.Stages[i].Name == stageStatus.Name { - stage = &playbook.Spec.Stages[i] + var stage *konveyoriov1alpha1.AgentWorkflowStage + for i := range workflow.Spec.Stages { + if workflow.Spec.Stages[i].Name == stageStatus.Name { + stage = &workflow.Spec.Stages[i] break } } if stage == nil { - // The playbook was modified and no longer has this stage. + // The workflow was modified and no longer has this stage. pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() pbRun.Status.CompletionTime = &now @@ -172,7 +177,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req Status: metav1.ConditionFalse, ObservedGeneration: pbRun.Generation, Reason: "StageNotFound", - Message: fmt.Sprintf("Stage %q no longer exists in AgentPlaybook %q", stageStatus.Name, pbRun.Spec.PlaybookRef), + Message: fmt.Sprintf("Stage %q no longer exists in AgentWorkflow %q", stageStatus.Name, pbRun.Spec.WorkflowRef), }) return r.patchRunStatus(ctx, &pbRun, original) } @@ -182,7 +187,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // If no AgentRun exists for this stage, create one. if stageStatus.AgentRunName == "" { - agentRunName, err := r.createAgentRunForStage(ctx, &pbRun, &playbook, stage) + agentRunName, err := r.createAgentRunForStage(ctx, &pbRun, &workflow, stage, stageIndex, len(pbRun.Status.Stages)) if err != nil { logger.Error(err, "Failed to create AgentRun for stage", "stage", stage.Name) @@ -248,7 +253,7 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req return r.patchRunStatus(ctx, &pbRun, original) case konveyoriov1alpha1.AgentRunPhaseFailed: - // Stage failed — fail the entire playbook run. + // Stage failed — fail the entire workflow run. pbRun.Status.Phase = konveyoriov1alpha1.AgentRunPhaseFailed now := metav1.Now() pbRun.Status.CompletionTime = &now @@ -276,8 +281,8 @@ func (r *AgentPlaybookRunReconciler) Reconcile(ctx context.Context, req ctrl.Req // findCurrentStageIndex returns the index of the first stage that has not // yet succeeded. Returns len(stages) if all stages have succeeded. -func (r *AgentPlaybookRunReconciler) findCurrentStageIndex( - pbRun *konveyoriov1alpha1.AgentPlaybookRun, +func (r *AgentWorkflowRunReconciler) findCurrentStageIndex( + pbRun *konveyoriov1alpha1.AgentWorkflowRun, ) int { for i, stage := range pbRun.Status.Stages { if stage.Phase != konveyoriov1alpha1.AgentRunPhaseSucceeded { @@ -294,33 +299,92 @@ func stageAgentRunName(pbRunName, stageName string) string { return sanitizeVolumeName(pbRunName + "-" + stageName) } -// createAgentRunForStage creates an AgentRun for the given playbook stage. -// It forwards params, models, env, and envFrom from the playbook run spec. -// Playbook-level instructions (Guide) are passed as a separate env var +// createAgentRunForStage creates an AgentRun for the given workflow stage. +// It forwards models, env, and envFrom from the workflow run spec. Params +// are filtered to only those the stage's Agent declares — this avoids +// forcing every stage Agent to declare every param from other stages. +// Workflow-level instructions (Guide) are passed as a separate env var // so the harness can present them alongside stage instructions without // the controller making prompt composition decisions. // -// Uses a deterministic name (-) so that duplicate +// Uses a deterministic name (-) so that duplicate // creation on status-patch conflict is caught by AlreadyExists. -func (r *AgentPlaybookRunReconciler) createAgentRunForStage( +func (r *AgentWorkflowRunReconciler) createAgentRunForStage( ctx context.Context, - pbRun *konveyoriov1alpha1.AgentPlaybookRun, - playbook *konveyoriov1alpha1.AgentPlaybook, - stage *konveyoriov1alpha1.AgentPlaybookStage, + pbRun *konveyoriov1alpha1.AgentWorkflowRun, + workflow *konveyoriov1alpha1.AgentWorkflow, + stage *konveyoriov1alpha1.AgentWorkflowStage, + stageIndex int, + stageCount int, ) (string, error) { agentRunName := stageAgentRunName(pbRun.Name, stage.Name) - // Pass playbook-level instructions (Guide) as an env var. - // The harness decides how to compose this with the Agent prompt - // and stage instructions. + // Look up the stage's Agent to determine which params it declares. + var agent konveyoriov1alpha1.Agent + if err := r.Get(ctx, types.NamespacedName{ + Name: stage.AgentRef, Namespace: pbRun.Namespace, + }, &agent); err != nil { + return "", fmt.Errorf("looking up Agent %q for stage %q: %w", stage.AgentRef, stage.Name, err) + } + + // Build a set of param names the stage Agent declares. + declared := make(map[string]bool, len(agent.Spec.Params)) + for _, p := range agent.Spec.Params { + declared[p.Name] = true + } + + // Filter workflow-run params to only those this stage's Agent declares. + // Params not declared by the stage Agent are silently dropped — log + // and emit an event so typos are debuggable. + var stageParams []konveyoriov1alpha1.AgentRunParam + var skipped []string + for _, p := range pbRun.Spec.Params { + if declared[p.Name] { + stageParams = append(stageParams, p) + } else { + skipped = append(skipped, p.Name) + } + } + if len(skipped) > 0 { + logger := log.FromContext(ctx) + logger.V(1).Info("Filtered undeclared params for stage", + "stage", stage.Name, + "agent", stage.AgentRef, + "skippedParams", skipped, + ) + r.Recorder.Eventf(pbRun, nil, corev1.EventTypeNormal, "ParamsFiltered", + "FilterParams", "Stage %q (Agent %q): skipped undeclared params: %s", + stage.Name, stage.AgentRef, strings.Join(skipped, ", ")) + } + + // User-supplied env vars first, then controller-owned vars last. + // Kubernetes uses last-entry-wins for duplicate names, so + // controller-injected vars cannot be overridden by user input. var env []corev1.EnvVar - if playbook.Spec.Guide != "" { + env = append(env, pbRun.Spec.Env...) + + // Controller-owned env vars — appended after user env so they + // cannot be spoofed. + if workflow.Spec.Guide != "" { env = append(env, corev1.EnvVar{ - Name: "KONVEYOR_PLAYBOOK_INSTRUCTIONS", - Value: playbook.Spec.Guide, + Name: "KONVEYOR_WORKFLOW_GUIDE", + Value: workflow.Spec.Guide, }) } - env = append(env, pbRun.Spec.Env...) + + // Stage metadata for the harness. Used for stage-aware token + // revocation: the harness revokes the Hub API token only on the + // last stage (#68). + env = append(env, + corev1.EnvVar{ + Name: "KONVEYOR_WORKFLOW_STAGE", + Value: fmt.Sprintf("%d", stageIndex+1), + }, + corev1.EnvVar{ + Name: "KONVEYOR_WORKFLOW_STAGE_COUNT", + Value: fmt.Sprintf("%d", stageCount), + }, + ) agentRun := &konveyoriov1alpha1.AgentRun{ ObjectMeta: metav1.ObjectMeta{ @@ -328,7 +392,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( Namespace: pbRun.Namespace, Labels: map[string]string{ labelManagedBy: managedByLabel, - labelAgentPlaybookRun: pbRun.Name, + labelAgentWorkflowRun: pbRun.Name, labelStage: stage.Name, }, }, @@ -336,7 +400,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( AgentRef: stage.AgentRef, Instructions: stage.Instructions, Models: pbRun.Spec.Models, - Params: pbRun.Spec.Params, + Params: stageParams, Env: env, EnvFrom: pbRun.Spec.EnvFrom, }, @@ -349,7 +413,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( if err := r.Create(ctx, agentRun); err != nil { if errors.IsAlreadyExists(err) { // AgentRun was likely created on a prior reconcile but the - // status patch failed. Verify it belongs to this playbook + // status patch failed. Verify it belongs to this workflow // run before accepting it. var existing konveyoriov1alpha1.AgentRun if getErr := r.Get(ctx, types.NamespacedName{ @@ -358,7 +422,7 @@ func (r *AgentPlaybookRunReconciler) createAgentRunForStage( return "", fmt.Errorf("fetching existing AgentRun %q: %w", agentRunName, getErr) } if !isOwnedBy(&existing, pbRun) { - return "", fmt.Errorf("AgentRun %q already exists but is not owned by this playbook run", agentRunName) + return "", fmt.Errorf("AgentRun %q already exists but is not owned by this workflow run", agentRunName) } return agentRunName, nil } @@ -379,65 +443,65 @@ func isOwnedBy(child client.Object, parent client.Object) bool { return false } -// patchRunStatus patches the AgentPlaybookRun status. -func (r *AgentPlaybookRunReconciler) patchRunStatus( +// patchRunStatus patches the AgentWorkflowRun status. +func (r *AgentWorkflowRunReconciler) patchRunStatus( ctx context.Context, - pbRun *konveyoriov1alpha1.AgentPlaybookRun, - original *konveyoriov1alpha1.AgentPlaybookRun, + pbRun *konveyoriov1alpha1.AgentWorkflowRun, + original *konveyoriov1alpha1.AgentWorkflowRun, ) (ctrl.Result, error) { if err := r.Status().Patch(ctx, pbRun, client.MergeFrom(original)); err != nil { - log.FromContext(ctx).Error(err, "Failed to patch AgentPlaybookRun status", - "agentPlaybookRun", pbRun.Name) + log.FromContext(ctx).Error(err, "Failed to patch AgentWorkflowRun status", + "agentWorkflowRun", pbRun.Name) return ctrl.Result{}, err } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. -func (r *AgentPlaybookRunReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Index AgentPlaybookRuns by playbookRef for efficient reverse lookup - // when an AgentPlaybook changes. +func (r *AgentWorkflowRunReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Index AgentWorkflowRuns by workflowRef for efficient reverse lookup + // when an AgentWorkflow changes. if err := mgr.GetFieldIndexer().IndexField( context.Background(), - &konveyoriov1alpha1.AgentPlaybookRun{}, - playbookRunRefIndexField, + &konveyoriov1alpha1.AgentWorkflowRun{}, + workflowRunRefIndexField, func(obj client.Object) []string { - pbRun := obj.(*konveyoriov1alpha1.AgentPlaybookRun) - return []string{pbRun.Spec.PlaybookRef} + pbRun := obj.(*konveyoriov1alpha1.AgentWorkflowRun) + return []string{pbRun.Spec.WorkflowRef} }, ); err != nil { - return fmt.Errorf("indexing %s: %w", playbookRunRefIndexField, err) + return fmt.Errorf("indexing %s: %w", workflowRunRefIndexField, err) } return ctrl.NewControllerManagedBy(mgr). - For(&konveyoriov1alpha1.AgentPlaybookRun{}). + For(&konveyoriov1alpha1.AgentWorkflowRun{}). Owns(&konveyoriov1alpha1.AgentRun{}). Watches( - &konveyoriov1alpha1.AgentPlaybook{}, - handler.EnqueueRequestsFromMapFunc(r.findRunsForPlaybook), + &konveyoriov1alpha1.AgentWorkflow{}, + handler.EnqueueRequestsFromMapFunc(r.findRunsForWorkflow), ). - Named("agentplaybookrun"). + Named("agentworkflowrun"). Complete(r) } -// findRunsForPlaybook returns reconcile requests for all non-terminal -// AgentPlaybookRuns that reference the given AgentPlaybook. -func (r *AgentPlaybookRunReconciler) findRunsForPlaybook( +// findRunsForWorkflow returns reconcile requests for all non-terminal +// AgentWorkflowRuns that reference the given AgentWorkflow. +func (r *AgentWorkflowRunReconciler) findRunsForWorkflow( ctx context.Context, obj client.Object, ) []reconcile.Request { - playbook, ok := obj.(*konveyoriov1alpha1.AgentPlaybook) + workflow, ok := obj.(*konveyoriov1alpha1.AgentWorkflow) if !ok { return nil } - var runList konveyoriov1alpha1.AgentPlaybookRunList + var runList konveyoriov1alpha1.AgentWorkflowRunList if err := r.List(ctx, &runList, - client.InNamespace(playbook.Namespace), - client.MatchingFields{playbookRunRefIndexField: playbook.Name}, + client.InNamespace(workflow.Namespace), + client.MatchingFields{workflowRunRefIndexField: workflow.Name}, ); err != nil { - log.FromContext(ctx).Error(err, "Failed to list AgentPlaybookRuns for AgentPlaybook", - "playbook", playbook.Name) + log.FromContext(ctx).Error(err, "Failed to list AgentWorkflowRuns for AgentWorkflow", + "workflow", workflow.Name) return nil } diff --git a/internal/controller/agentplaybookrun_controller_test.go b/internal/controller/agentworkflowrun_controller_test.go similarity index 58% rename from internal/controller/agentplaybookrun_controller_test.go rename to internal/controller/agentworkflowrun_controller_test.go index 1d46fd3c..31a56c6e 100644 --- a/internal/controller/agentplaybookrun_controller_test.go +++ b/internal/controller/agentworkflowrun_controller_test.go @@ -44,11 +44,11 @@ func updateAgentRunStatus(name string, mutate func(*konveyoriov1alpha1.AgentRun) }, 10*time.Second, 250*time.Millisecond).Should(Succeed()) } -// waitForPlaybookReady waits until the named AgentPlaybook has Ready=True. -func waitForPlaybookReady(playbookName string) { - key := types.NamespacedName{Name: playbookName, Namespace: testNamespace} +// waitForWorkflowReady waits until the named AgentWorkflow has Ready=True. +func waitForWorkflowReady(workflowName string) { + key := types.NamespacedName{Name: workflowName, Namespace: testNamespace} EventuallyWithOffset(1, func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybook + var fetched konveyoriov1alpha1.AgentWorkflow g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) @@ -56,41 +56,41 @@ func waitForPlaybookReady(playbookName string) { }, 10*time.Second, 250*time.Millisecond).Should(Succeed()) } -var _ = Describe("AgentPlaybookRun Controller", func() { +var _ = Describe("AgentWorkflowRun Controller", func() { const ( timeout = 10 * time.Second interval = 250 * time.Millisecond ) - Context("when the referenced AgentPlaybook does not exist", func() { - const name = "apr-ctrl-no-playbook" + Context("when the referenced AgentWorkflow does not exist", func() { + const name = "apr-ctrl-no-workflow" - It("should set Phase=Failed with PlaybookNotFound", func() { - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + It("should set Phase=Failed with WorkflowNotFound", func() { + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "nonexistent-playbook", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "nonexistent-workflow", }, } Expect(k8sClient.Create(ctx, pbRun)).To(Succeed()) key := types.NamespacedName{Name: name, Namespace: testNamespace} Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, key, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseFailed)) readyCond := meta.FindStatusCondition(fetched.Status.Conditions, ConditionTypeReady) g.Expect(readyCond).NotTo(BeNil()) - g.Expect(readyCond.Reason).To(Equal("PlaybookNotFound")) + g.Expect(readyCond.Reason).To(Equal("WorkflowNotFound")) }, timeout, interval).Should(Succeed()) Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) }) }) - Context("when the playbook is valid and stages execute sequentially", func() { + Context("when the workflow is valid and stages execute sequentially", func() { const ( - playbookName = "apr-ctrl-seq-playbook" + workflowName = "apr-ctrl-seq-workflow" pbRunName = "apr-ctrl-seq-run" agentName = "apr-ctrl-seq-agent" provName = "apr-prov-seq" @@ -115,25 +115,25 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(k8sClient.Create(ctx, agent)).To(Succeed()) waitForAgentReady(agentName) - By("creating a Ready AgentPlaybook with two stages") - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Guide: "Sequential test playbook", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + By("creating a Ready AgentWorkflow with two stages") + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Guide: "Sequential test workflow", + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "stage-a", AgentRef: agentName, Instructions: "Do stage A"}, {Name: "stage-b", AgentRef: agentName, Instructions: "Do stage B"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) - waitForPlaybookReady(playbookName) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) - By("creating the AgentPlaybookRun") - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + By("creating the AgentWorkflowRun") + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: playbookName, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, }, @@ -148,7 +148,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} expectedStageAName := stageAgentRunName(pbRunName, "stage-a") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseRunning)) g.Expect(fetched.Status.CurrentStage).To(Equal("stage-a")) @@ -171,11 +171,11 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(stageARun.Spec.Models[0].Role).To(Equal(testRolePrimary)) By("verifying stage-a AgentRun has correct labels") - Expect(stageARun.Labels).To(HaveKeyWithValue(labelAgentPlaybookRun, pbRunName)) + Expect(stageARun.Labels).To(HaveKeyWithValue(labelAgentWorkflowRun, pbRunName)) Expect(stageARun.Labels).To(HaveKeyWithValue(labelStage, "stage-a")) By("verifying stage-b is not started yet") - var fetchedPBRun konveyoriov1alpha1.AgentPlaybookRun + var fetchedPBRun konveyoriov1alpha1.AgentWorkflowRun Expect(k8sClient.Get(ctx, pbRunKey, &fetchedPBRun)).To(Succeed()) Expect(fetchedPBRun.Status.Stages[1].AgentRunName).To(BeEmpty()) Expect(fetchedPBRun.Status.Stages[1].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhasePending)) @@ -193,7 +193,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { By("verifying stage-b AgentRun is created") var stageBRunName string Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.CurrentStage).To(Equal("stage-b")) g.Expect(fetched.Status.Stages[0].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseSucceeded)) @@ -219,9 +219,9 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }) }) - By("verifying the playbook run completes successfully") + By("verifying the workflow run completes successfully") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseSucceeded)) g.Expect(fetched.Status.CompletionTime).NotTo(BeNil()) @@ -236,27 +236,159 @@ var _ = Describe("AgentPlaybookRun Controller", func() { var runList konveyoriov1alpha1.AgentRunList Expect(k8sClient.List(ctx, &runList, client.InNamespace(testNamespace), - client.MatchingLabels{labelAgentPlaybookRun: pbRunName}, + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, )).To(Succeed()) for i := range runList.Items { Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) } Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent)).To(Succeed()) }) }) + Context("when stages use different Agents with different params", func() { + const ( + workflowName = "apr-ctrl-filter-workflow" + pbRunName = "apr-ctrl-filter-run" + agentAName = "apr-ctrl-filter-agent-a" + agentBName = "apr-ctrl-filter-agent-b" + provName = "apr-prov-filter" + secretName = "apr-secret-filter" + ) + + It("should forward only params each stage Agent declares", func() { + cleanup := makeReadyProvider(provName, secretName) + defer cleanup() + + By("creating Agent A that declares 'source_url' only") + agentA := &konveyoriov1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentAName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentSpec{ + Image: testAgentImage, + Providers: []konveyoriov1alpha1.AgentProviderRef{{Ref: provName}}, + Params: []konveyoriov1alpha1.AgentParam{ + {Name: "source_url", Required: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, agentA)).To(Succeed()) + waitForAgentReady(agentAName) + + By("creating Agent B that declares 'target_branch' only") + agentB := &konveyoriov1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: agentBName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentSpec{ + Image: testAgentImage, + Providers: []konveyoriov1alpha1.AgentProviderRef{{Ref: provName}}, + Params: []konveyoriov1alpha1.AgentParam{ + {Name: testParamTargetBranch, Required: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, agentB)).To(Succeed()) + waitForAgentReady(agentBName) + + By("creating a workflow with two stages using different Agents") + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ + {Name: "stage-a", AgentRef: agentAName}, + {Name: "stage-b", AgentRef: agentBName}, + }, + }, + } + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) + + By("creating the workflow run with params for both stages") + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ + ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, + Models: []konveyoriov1alpha1.AgentRunModelSelection{ + {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, + }, + Params: []konveyoriov1alpha1.AgentRunParam{ + {Name: "source_url", Value: "https://github.com/example/repo.git"}, + {Name: testParamTargetBranch, Value: "konveyor/test"}, + }, + }, + } + Expect(k8sClient.Create(ctx, pbRun)).To(Succeed()) + + By("verifying stage-a AgentRun gets only 'source_url'") + pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} + expectedStageAName := stageAgentRunName(pbRunName, "stage-a") + Eventually(func(g Gomega) { + var fetched konveyoriov1alpha1.AgentWorkflowRun + g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) + g.Expect(fetched.Status.Stages).To(HaveLen(2)) + g.Expect(fetched.Status.Stages[0].AgentRunName).To(Equal(expectedStageAName)) + }, timeout, interval).Should(Succeed()) + + var stageARun konveyoriov1alpha1.AgentRun + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: expectedStageAName, Namespace: testNamespace, + }, &stageARun)).To(Succeed()) + Expect(stageARun.Spec.Params).To(HaveLen(1)) + Expect(stageARun.Spec.Params[0].Name).To(Equal("source_url")) + + By("simulating stage-a success to advance to stage-b") + updateAgentRunStatus(expectedStageAName, func(run *konveyoriov1alpha1.AgentRun) { + run.Status.Phase = konveyoriov1alpha1.AgentRunPhaseSucceeded + now := metav1.Now() + run.Status.CompletionTime = &now + meta.SetStatusCondition(&run.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: reasonSucceeded, + }) + }) + + By("verifying stage-b AgentRun gets only 'target_branch'") + expectedStageBName := stageAgentRunName(pbRunName, "stage-b") + Eventually(func(g Gomega) { + var fetched konveyoriov1alpha1.AgentWorkflowRun + g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) + g.Expect(fetched.Status.Stages).To(HaveLen(2)) + g.Expect(fetched.Status.Stages[1].AgentRunName).To(Equal(expectedStageBName)) + }, timeout, interval).Should(Succeed()) + + var stageBRun konveyoriov1alpha1.AgentRun + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: expectedStageBName, Namespace: testNamespace, + }, &stageBRun)).To(Succeed()) + Expect(stageBRun.Spec.Params).To(HaveLen(1)) + Expect(stageBRun.Spec.Params[0].Name).To(Equal(testParamTargetBranch)) + + By("cleaning up") + var runList konveyoriov1alpha1.AgentRunList + Expect(k8sClient.List(ctx, &runList, + client.InNamespace(testNamespace), + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, + )).To(Succeed()) + for i := range runList.Items { + Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) + } + Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) + Expect(k8sClient.Delete(ctx, agentA)).To(Succeed()) + Expect(k8sClient.Delete(ctx, agentB)).To(Succeed()) + }) + }) + Context("when a stage fails", func() { const ( - playbookName = "apr-ctrl-fail-playbook" + workflowName = "apr-ctrl-fail-workflow" pbRunName = "apr-ctrl-fail-run" agentName = "apr-ctrl-fail-agent" provName = "apr-prov-fail" secretName = "apr-secret-fail" ) - It("should fail the entire playbook run", func() { + It("should fail the entire workflow run", func() { cleanup := makeReadyProvider(provName, secretName) defer cleanup() @@ -270,22 +402,22 @@ var _ = Describe("AgentPlaybookRun Controller", func() { Expect(k8sClient.Create(ctx, agent)).To(Succeed()) waitForAgentReady(agentName) - playbook := &konveyoriov1alpha1.AgentPlaybook{ - ObjectMeta: metav1.ObjectMeta{Name: playbookName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + workflow := &konveyoriov1alpha1.AgentWorkflow{ + ObjectMeta: metav1.ObjectMeta{Name: workflowName, Namespace: testNamespace}, + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "will-fail", AgentRef: agentName, Instructions: "This will fail"}, {Name: "never-runs", AgentRef: agentName, Instructions: "Should not run"}, }, }, } - Expect(k8sClient.Create(ctx, playbook)).To(Succeed()) - waitForPlaybookReady(playbookName) + Expect(k8sClient.Create(ctx, workflow)).To(Succeed()) + waitForWorkflowReady(workflowName) - pbRun := &konveyoriov1alpha1.AgentPlaybookRun{ + pbRun := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{Name: pbRunName, Namespace: testNamespace}, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: playbookName, + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: workflowName, Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: provName, Model: testLLMModelName}, }, @@ -297,7 +429,7 @@ var _ = Describe("AgentPlaybookRun Controller", func() { pbRunKey := types.NamespacedName{Name: pbRunName, Namespace: testNamespace} var stageRunName string Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Stages).To(HaveLen(2)) g.Expect(fetched.Status.Stages[0].AgentRunName).NotTo(BeEmpty()) @@ -314,9 +446,9 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }) }) - By("verifying the playbook run fails") + By("verifying the workflow run fails") Eventually(func(g Gomega) { - var fetched konveyoriov1alpha1.AgentPlaybookRun + var fetched konveyoriov1alpha1.AgentWorkflowRun g.Expect(k8sClient.Get(ctx, pbRunKey, &fetched)).To(Succeed()) g.Expect(fetched.Status.Phase).To(Equal(konveyoriov1alpha1.AgentRunPhaseFailed)) g.Expect(fetched.Status.CompletionTime).NotTo(BeNil()) @@ -326,23 +458,23 @@ var _ = Describe("AgentPlaybookRun Controller", func() { }, timeout, interval).Should(Succeed()) By("verifying stage-2 was never started") - var finalPBRun konveyoriov1alpha1.AgentPlaybookRun + var finalPBRun konveyoriov1alpha1.AgentWorkflowRun Expect(k8sClient.Get(ctx, pbRunKey, &finalPBRun)).To(Succeed()) Expect(finalPBRun.Status.Stages[1].AgentRunName).To(BeEmpty()) Expect(finalPBRun.Status.Stages[1].Phase).To(Equal(konveyoriov1alpha1.AgentRunPhasePending)) - // Clean up — delete the AgentRuns owned by the playbook run + // Clean up — delete the AgentRuns owned by the workflow run // first to avoid GC issues in tests. var runList konveyoriov1alpha1.AgentRunList Expect(k8sClient.List(ctx, &runList, client.InNamespace(testNamespace), - client.MatchingLabels{labelAgentPlaybookRun: pbRunName}, + client.MatchingLabels{labelAgentWorkflowRun: pbRunName}, )).To(Succeed()) for i := range runList.Items { Expect(k8sClient.Delete(ctx, &runList.Items[i])).To(Succeed()) } Expect(k8sClient.Delete(ctx, pbRun)).To(Succeed()) - Expect(k8sClient.Delete(ctx, playbook)).To(Succeed()) + Expect(k8sClient.Delete(ctx, workflow)).To(Succeed()) Expect(k8sClient.Delete(ctx, agent)).To(Succeed()) }) }) diff --git a/internal/controller/crd_validation_test.go b/internal/controller/crd_validation_test.go index 94f8fa20..a470e0d7 100644 --- a/internal/controller/crd_validation_test.go +++ b/internal/controller/crd_validation_test.go @@ -30,10 +30,11 @@ import ( ) const ( - testImageGoose = "quay.io/konveyor/agent-java-goose:latest" - testParamName = "source_url" - testProvider = "anthropic-provider" - testModel = "claude-sonnet-4-20250514" + testImageGoose = "quay.io/konveyor/agent-java-goose:latest" + testParamName = "source_url" + testParamTargetBranch = "target_branch" + testProvider = "anthropic-provider" + testModel = "claude-sonnet-4-20250514" ) var _ = Describe("CRD Validation", func() { @@ -236,7 +237,7 @@ var _ = Describe("CRD Validation", func() { }, Params: []konveyoriov1alpha1.AgentParam{ { - Name: "target_branch", + Name: testParamTargetBranch, Default: testDefaultBranch, // required is omitted — this must not fail }, @@ -376,17 +377,17 @@ var _ = Describe("CRD Validation", func() { }) }) - // ── AgentPlaybook ────────────────────────────────────────────────── - Context("AgentPlaybook", func() { - It("should accept a valid AgentPlaybook", func() { - ap := &konveyoriov1alpha1.AgentPlaybook{ + // ── AgentWorkflow ────────────────────────────────────────────────── + Context("AgentWorkflow", func() { + It("should accept a valid AgentWorkflow", func() { + ap := &konveyoriov1alpha1.AgentWorkflow{ ObjectMeta: metav1.ObjectMeta{ Name: "ap-valid-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Guide: "Migrate a Java EE application to Quarkus.", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{ + Stages: []konveyoriov1alpha1.AgentWorkflowStage{ {Name: "discover", AgentRef: "discovery-agent", Instructions: "Analyze the app."}, {Name: "implement", AgentRef: "migration-agent", Instructions: "Execute migration."}, }, @@ -396,15 +397,15 @@ var _ = Describe("CRD Validation", func() { Expect(k8sClient.Delete(ctx, ap)).To(Succeed()) }) - It("should reject an AgentPlaybook with empty stages", func() { - ap := &konveyoriov1alpha1.AgentPlaybook{ + It("should reject an AgentWorkflow with empty stages", func() { + ap := &konveyoriov1alpha1.AgentWorkflow{ ObjectMeta: metav1.ObjectMeta{ Name: "ap-empty-stages-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookSpec{ + Spec: konveyoriov1alpha1.AgentWorkflowSpec{ Guide: "No stages here.", - Stages: []konveyoriov1alpha1.AgentPlaybookStage{}, + Stages: []konveyoriov1alpha1.AgentWorkflowStage{}, }, } err := k8sClient.Create(ctx, ap) @@ -458,16 +459,16 @@ var _ = Describe("CRD Validation", func() { }) }) - // ── AgentPlaybookRun ─────────────────────────────────────────────── - Context("AgentPlaybookRun", func() { - It("should accept a valid AgentPlaybookRun", func() { - apr := &konveyoriov1alpha1.AgentPlaybookRun{ + // ── AgentWorkflowRun ─────────────────────────────────────────────── + Context("AgentWorkflowRun", func() { + It("should accept a valid AgentWorkflowRun", func() { + apr := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{ Name: "apr-valid-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "java-migration", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "java-migration", Models: []konveyoriov1alpha1.AgentRunModelSelection{ {Role: testRolePrimary, Provider: "anthropic", Model: testModel}, }, @@ -488,25 +489,25 @@ var _ = Describe("CRD Validation", func() { Expect(k8sClient.Delete(ctx, apr)).To(Succeed()) }) - It("should reject mutation of playbookRef", func() { - apr := &konveyoriov1alpha1.AgentPlaybookRun{ + It("should reject mutation of workflowRef", func() { + apr := &konveyoriov1alpha1.AgentWorkflowRun{ ObjectMeta: metav1.ObjectMeta{ Name: "apr-immutable-test", Namespace: testNamespace, }, - Spec: konveyoriov1alpha1.AgentPlaybookRunSpec{ - PlaybookRef: "original-playbook", + Spec: konveyoriov1alpha1.AgentWorkflowRunSpec{ + WorkflowRef: "original-workflow", }, } Expect(k8sClient.Create(ctx, apr)).To(Succeed()) - apr.Spec.PlaybookRef = "different-playbook" + apr.Spec.WorkflowRef = "different-workflow" err := k8sClient.Update(ctx, apr) Expect(err).To(HaveOccurred()) Expect(errors.IsInvalid(err)).To(BeTrue(), fmt.Sprintf("expected Invalid error, got: %v", err)) // Clean up - apr.Spec.PlaybookRef = "original-playbook" + apr.Spec.WorkflowRef = "original-workflow" Expect(k8sClient.Delete(ctx, apr)).To(Succeed()) }) }) diff --git a/internal/controller/doc.go b/internal/controller/doc.go index a1fe4de9..6bc9b04a 100644 --- a/internal/controller/doc.go +++ b/internal/controller/doc.go @@ -33,10 +33,10 @@ const ( // labelAgent identifies resources belonging to an Agent. labelAgent = "konveyor.io/agent" - // labelAgentPlaybookRun identifies resources belonging to an AgentPlaybookRun. - labelAgentPlaybookRun = "konveyor.io/agentplaybookrun" + // labelAgentWorkflowRun identifies resources belonging to an AgentWorkflowRun. + labelAgentWorkflowRun = "konveyor.io/agentworkflowrun" - // labelStage identifies the playbook stage a resource belongs to. + // labelStage identifies the workflow stage a resource belongs to. labelStage = "konveyor.io/stage" // reasonSucceeded is the condition reason for successful completion. diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index ba84623c..942a7b78 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -148,15 +148,16 @@ var _ = BeforeSuite(func() { }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&AgentPlaybookReconciler{ + err = (&AgentWorkflowReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) - err = (&AgentPlaybookRunReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + err = (&AgentWorkflowRunReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("agentworkflowrun-controller"), }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred())