Skip to content

Commit 0263458

Browse files
yroblataskbot
andauthored
Add OnDecline and OnCancel handlers for workflow steps (#2961)
* Add OnDecline and OnCancel handlers for workflow steps Add support for elicitation response handlers in VirtualMCPServer workflow steps, allowing users to configure behavior when elicitation requests are declined or canceled by the user. The implementation adds: - ElicitationResponseHandler type with action field (skip_remaining, abort, continue) - OnDecline field to WorkflowStep for handling explicit decline responses - OnCancel field to WorkflowStep for handling cancel/dismiss actions - Converter logic to map CRD fields to vmcp config types - Comprehensive test coverage for all action types and edge cases These fields were already supported by the implementation in pkg/vmcp/config/config.go but were missing from the CRD definition, preventing users from configuring this behavior. Fixes #2772 * fix ci * add examples and tests --------- Co-authored-by: taskbot <[email protected]>
1 parent 1976761 commit 0263458

File tree

13 files changed

+1511
-3
lines changed

13 files changed

+1511
-3
lines changed

cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ type WorkflowStep struct {
296296
// +kubebuilder:validation:Type=object
297297
Schema *runtime.RawExtension `json:"schema,omitempty"`
298298

299+
// OnDecline defines the action to take when the user explicitly declines the elicitation
300+
// Only used when Type is "elicitation"
301+
// +optional
302+
OnDecline *ElicitationResponseHandler `json:"onDecline,omitempty"`
303+
304+
// OnCancel defines the action to take when the user cancels/dismisses the elicitation
305+
// Only used when Type is "elicitation"
306+
// +optional
307+
OnCancel *ElicitationResponseHandler `json:"onCancel,omitempty"`
308+
299309
// DependsOn lists step IDs that must complete before this step
300310
// +optional
301311
DependsOn []string `json:"dependsOn,omitempty"`
@@ -333,6 +343,18 @@ type ErrorHandling struct {
333343
RetryDelay string `json:"retryDelay,omitempty"`
334344
}
335345

346+
// ElicitationResponseHandler defines how to handle user responses to elicitation requests
347+
type ElicitationResponseHandler struct {
348+
// Action defines the action to take when the user declines or cancels
349+
// - skip_remaining: Skip remaining steps in the workflow
350+
// - abort: Abort the entire workflow execution
351+
// - continue: Continue to the next step
352+
// +kubebuilder:validation:Enum=skip_remaining;abort;continue
353+
// +kubebuilder:default=abort
354+
// +optional
355+
Action string `json:"action,omitempty"`
356+
}
357+
336358
// OperationalConfig defines operational settings
337359
type OperationalConfig struct {
338360
// LogLevel sets the logging level for the Virtual MCP server.

cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,12 @@ func validateCompositeToolStep(
269269
}
270270

271271
// Validate error handling
272-
return validateStepErrorHandling(toolIndex, stepIndex, step)
272+
if err := validateStepErrorHandling(toolIndex, stepIndex, step); err != nil {
273+
return err
274+
}
275+
276+
// Validate elicitation response handlers
277+
return validateStepElicitationResponseHandlers(toolIndex, stepIndex, step)
273278
}
274279

275280
// validateStepType validates the step type and type-specific requirements
@@ -347,3 +352,30 @@ func validateStepErrorHandling(toolIndex, stepIndex int, step WorkflowStep) erro
347352

348353
return nil
349354
}
355+
356+
// validateStepElicitationResponseHandlers validates elicitation response handler configuration for a step
357+
func validateStepElicitationResponseHandlers(toolIndex, stepIndex int, step WorkflowStep) error {
358+
validActions := map[string]bool{
359+
"skip_remaining": true,
360+
"abort": true,
361+
"continue": true,
362+
}
363+
364+
// Validate OnDecline action
365+
if step.OnDecline != nil && step.OnDecline.Action != "" {
366+
if !validActions[step.OnDecline.Action] {
367+
return fmt.Errorf("spec.compositeTools[%d].steps[%d].onDecline.action must be one of: skip_remaining, abort, continue",
368+
toolIndex, stepIndex)
369+
}
370+
}
371+
372+
// Validate OnCancel action
373+
if step.OnCancel != nil && step.OnCancel.Action != "" {
374+
if !validActions[step.OnCancel.Action] {
375+
return fmt.Errorf("spec.compositeTools[%d].steps[%d].onCancel.action must be one of: skip_remaining, abort, continue",
376+
toolIndex, stepIndex)
377+
}
378+
}
379+
380+
return nil
381+
}

cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,236 @@ func TestValidateCompositeToolsWithDependencies(t *testing.T) {
691691
wantErr: true,
692692
errMsg: "spec.compositeTools[0].steps[0].dependsOn references unknown step \"unknown-step\"",
693693
},
694+
{
695+
name: "valid elicitation with OnDecline skip_remaining",
696+
vmcp: &VirtualMCPServer{
697+
Spec: VirtualMCPServerSpec{
698+
GroupRef: GroupRef{Name: "test-group"},
699+
CompositeTools: []CompositeToolSpec{
700+
{
701+
Name: "test-tool",
702+
Description: "Test composite tool",
703+
Steps: []WorkflowStep{
704+
{
705+
ID: "step1",
706+
Type: WorkflowStepTypeElicitation,
707+
Message: "Please provide input",
708+
OnDecline: &ElicitationResponseHandler{
709+
Action: "skip_remaining",
710+
},
711+
},
712+
},
713+
},
714+
},
715+
},
716+
},
717+
wantErr: false,
718+
},
719+
{
720+
name: "valid elicitation with OnDecline abort",
721+
vmcp: &VirtualMCPServer{
722+
Spec: VirtualMCPServerSpec{
723+
GroupRef: GroupRef{Name: "test-group"},
724+
CompositeTools: []CompositeToolSpec{
725+
{
726+
Name: "test-tool",
727+
Description: "Test composite tool",
728+
Steps: []WorkflowStep{
729+
{
730+
ID: "step1",
731+
Type: WorkflowStepTypeElicitation,
732+
Message: "Please provide input",
733+
OnDecline: &ElicitationResponseHandler{
734+
Action: "abort",
735+
},
736+
},
737+
},
738+
},
739+
},
740+
},
741+
},
742+
wantErr: false,
743+
},
744+
{
745+
name: "valid elicitation with OnDecline continue",
746+
vmcp: &VirtualMCPServer{
747+
Spec: VirtualMCPServerSpec{
748+
GroupRef: GroupRef{Name: "test-group"},
749+
CompositeTools: []CompositeToolSpec{
750+
{
751+
Name: "test-tool",
752+
Description: "Test composite tool",
753+
Steps: []WorkflowStep{
754+
{
755+
ID: "step1",
756+
Type: WorkflowStepTypeElicitation,
757+
Message: "Please provide input",
758+
OnDecline: &ElicitationResponseHandler{
759+
Action: "continue",
760+
},
761+
},
762+
},
763+
},
764+
},
765+
},
766+
},
767+
wantErr: false,
768+
},
769+
{
770+
name: "valid elicitation with OnCancel skip_remaining",
771+
vmcp: &VirtualMCPServer{
772+
Spec: VirtualMCPServerSpec{
773+
GroupRef: GroupRef{Name: "test-group"},
774+
CompositeTools: []CompositeToolSpec{
775+
{
776+
Name: "test-tool",
777+
Description: "Test composite tool",
778+
Steps: []WorkflowStep{
779+
{
780+
ID: "step1",
781+
Type: WorkflowStepTypeElicitation,
782+
Message: "Please provide input",
783+
OnCancel: &ElicitationResponseHandler{
784+
Action: "skip_remaining",
785+
},
786+
},
787+
},
788+
},
789+
},
790+
},
791+
},
792+
wantErr: false,
793+
},
794+
{
795+
name: "valid elicitation with OnCancel abort",
796+
vmcp: &VirtualMCPServer{
797+
Spec: VirtualMCPServerSpec{
798+
GroupRef: GroupRef{Name: "test-group"},
799+
CompositeTools: []CompositeToolSpec{
800+
{
801+
Name: "test-tool",
802+
Description: "Test composite tool",
803+
Steps: []WorkflowStep{
804+
{
805+
ID: "step1",
806+
Type: WorkflowStepTypeElicitation,
807+
Message: "Please provide input",
808+
OnCancel: &ElicitationResponseHandler{
809+
Action: "abort",
810+
},
811+
},
812+
},
813+
},
814+
},
815+
},
816+
},
817+
wantErr: false,
818+
},
819+
{
820+
name: "valid elicitation with OnCancel continue",
821+
vmcp: &VirtualMCPServer{
822+
Spec: VirtualMCPServerSpec{
823+
GroupRef: GroupRef{Name: "test-group"},
824+
CompositeTools: []CompositeToolSpec{
825+
{
826+
Name: "test-tool",
827+
Description: "Test composite tool",
828+
Steps: []WorkflowStep{
829+
{
830+
ID: "step1",
831+
Type: WorkflowStepTypeElicitation,
832+
Message: "Please provide input",
833+
OnCancel: &ElicitationResponseHandler{
834+
Action: "continue",
835+
},
836+
},
837+
},
838+
},
839+
},
840+
},
841+
},
842+
wantErr: false,
843+
},
844+
{
845+
name: "valid elicitation with both OnDecline and OnCancel",
846+
vmcp: &VirtualMCPServer{
847+
Spec: VirtualMCPServerSpec{
848+
GroupRef: GroupRef{Name: "test-group"},
849+
CompositeTools: []CompositeToolSpec{
850+
{
851+
Name: "test-tool",
852+
Description: "Test composite tool",
853+
Steps: []WorkflowStep{
854+
{
855+
ID: "step1",
856+
Type: WorkflowStepTypeElicitation,
857+
Message: "Please provide input",
858+
OnDecline: &ElicitationResponseHandler{
859+
Action: "skip_remaining",
860+
},
861+
OnCancel: &ElicitationResponseHandler{
862+
Action: "abort",
863+
},
864+
},
865+
},
866+
},
867+
},
868+
},
869+
},
870+
wantErr: false,
871+
},
872+
{
873+
name: "invalid elicitation - OnDecline with invalid action",
874+
vmcp: &VirtualMCPServer{
875+
Spec: VirtualMCPServerSpec{
876+
GroupRef: GroupRef{Name: "test-group"},
877+
CompositeTools: []CompositeToolSpec{
878+
{
879+
Name: "test-tool",
880+
Description: "Test composite tool",
881+
Steps: []WorkflowStep{
882+
{
883+
ID: "step1",
884+
Type: WorkflowStepTypeElicitation,
885+
Message: "Please provide input",
886+
OnDecline: &ElicitationResponseHandler{
887+
Action: "invalid-action",
888+
},
889+
},
890+
},
891+
},
892+
},
893+
},
894+
},
895+
wantErr: true,
896+
errMsg: "spec.compositeTools[0].steps[0].onDecline.action must be one of: skip_remaining, abort, continue",
897+
},
898+
{
899+
name: "invalid elicitation - OnCancel with invalid action",
900+
vmcp: &VirtualMCPServer{
901+
Spec: VirtualMCPServerSpec{
902+
GroupRef: GroupRef{Name: "test-group"},
903+
CompositeTools: []CompositeToolSpec{
904+
{
905+
Name: "test-tool",
906+
Description: "Test composite tool",
907+
Steps: []WorkflowStep{
908+
{
909+
ID: "step1",
910+
Type: WorkflowStepTypeElicitation,
911+
Message: "Please provide input",
912+
OnCancel: &ElicitationResponseHandler{
913+
Action: "invalid-action",
914+
},
915+
},
916+
},
917+
},
918+
},
919+
},
920+
},
921+
wantErr: true,
922+
errMsg: "spec.compositeTools[0].steps[0].onCancel.action must be one of: skip_remaining, abort, continue",
923+
},
694924
}
695925

696926
for _, tt := range tests {

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/pkg/vmcpconfig/converter.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,19 @@ func (*Converter) convertWorkflowSteps(
723723
step.OnError = stepError
724724
}
725725

726+
// Convert elicitation response handlers
727+
if crdStep.OnDecline != nil {
728+
step.OnDecline = &vmcpconfig.ElicitationResponseConfig{
729+
Action: crdStep.OnDecline.Action,
730+
}
731+
}
732+
733+
if crdStep.OnCancel != nil {
734+
step.OnCancel = &vmcpconfig.ElicitationResponseConfig{
735+
Action: crdStep.OnCancel.Action,
736+
}
737+
}
738+
726739
workflowSteps = append(workflowSteps, step)
727740
}
728741

0 commit comments

Comments
 (0)