File Coverage

blib/lib/SignalWire/Agents/Skills/SkillManager.pm
Criterion Covered Total %
statement 56 63 88.8
branch 19 28 67.8
condition 8 16 50.0
subroutine 8 8 100.0
pod 0 4 0.0
total 91 119 76.4


line stmt bran cond sub pod time code
1             package SignalWire::Agents::Skills::SkillManager;
2             # Copyright (c) 2025 SignalWire
3             # Licensed under the MIT License.
4              
5 2     2   1909 use strict;
  2         4  
  2         93  
6 2     2   13 use warnings;
  2         3  
  2         169  
7 2     2   31 use Moo;
  2         6  
  2         15  
8 2     2   912 use Carp qw(croak);
  2         5  
  2         1949  
9              
10             has agent => (is => 'ro', required => 1, weak_ref => 1);
11             has loaded_skills => (is => 'rw', default => sub { {} });
12              
13             sub load_skill {
14 12     12 0 1330 my ($self, $skill_name, $skill_class, $params) = @_;
15 12   100     128 $params //= {};
16              
17             # Get class from registry if not provided
18 12 50       162 if (!$skill_class) {
19 12         708 require SignalWire::Agents::Skills::SkillRegistry;
20 12         117 my $factory = SignalWire::Agents::Skills::SkillRegistry->get_factory($skill_name);
21 12 100       279 unless ($factory) {
22 1         8 return (0, "Skill '$skill_name' not found in registry");
23             }
24 11         31 $skill_class = $factory;
25             }
26              
27             # Create instance
28 11         25 my $instance = eval {
29 11         404 $skill_class->new(
30             agent => $self->agent,
31             params => { %$params },
32             );
33             };
34 11 50       43 if ($@) {
35 0         0 return (0, "Failed to create skill '$skill_name': $@");
36             }
37              
38 11         90 my $instance_key = $instance->get_instance_key;
39              
40             # Check for duplicates
41 11 100       59 if (exists $self->loaded_skills->{$instance_key}) {
42 1 50       7 if (!$instance->supports_multiple_instances) {
43 1         11 return (0, "Skill '$skill_name' already loaded and does not support multiple instances");
44             }
45             }
46              
47             # Validate env vars
48 10 50       47 unless ($instance->validate_env_vars) {
49 0         0 return (0, "Skill '$skill_name' missing required environment variables");
50             }
51              
52             # Setup
53 10         22 my $ok = eval { $instance->setup };
  10         43  
54 10 50 33     63 if ($@ || !$ok) {
55 0   0     0 my $err = $@ || 'setup() returned false';
56 0         0 return (0, "Skill '$skill_name' setup failed: $err");
57             }
58              
59             # Register tools
60 10         22 eval { $instance->register_tools };
  10         39  
61 10 50       32 if ($@) {
62 0         0 return (0, "Skill '$skill_name' register_tools failed: $@");
63             }
64              
65             # Merge hints
66 10         52 my $hints = $instance->get_hints;
67 10 100 66     55 if ($hints && @$hints) {
68 1         11 $self->agent->add_hints(@$hints);
69             }
70              
71             # Merge global data
72 10         75 my $gdata = $instance->get_global_data;
73 10 50 33     48 if ($gdata && %$gdata) {
74 0         0 $self->agent->update_global_data($gdata);
75             }
76              
77             # Add prompt sections
78 10         64 my $sections = $instance->get_prompt_sections;
79 10 100 66     52 if ($sections && @$sections) {
80 8         24 for my $sec (@$sections) {
81             $self->agent->prompt_add_section(
82             $sec->{title},
83             $sec->{body},
84 8 50       67 ($sec->{bullets} ? (bullets => $sec->{bullets}) : ()),
85             );
86             }
87             }
88              
89 10         135 $self->loaded_skills->{$instance_key} = $instance;
90 10         79 return (1, '');
91             }
92              
93             sub unload_skill {
94 1     1 0 5 my ($self, $key) = @_;
95 1         7 my $instance = delete $self->loaded_skills->{$key};
96 1 50       5 if ($instance) {
97 1         3 eval { $instance->cleanup };
  1         11  
98 1         8 return 1;
99             }
100 0         0 return 0;
101             }
102              
103             sub list_skills {
104 1     1 0 6 my ($self) = @_;
105 1         2 return [ keys %{ $self->loaded_skills } ];
  1         7  
106             }
107              
108             sub has_skill {
109 5     5 0 1164 my ($self, $key) = @_;
110 5 100       42 return exists $self->loaded_skills->{$key} ? 1 : 0;
111             }
112              
113             1;