File Coverage

blib/lib/Sim/Agent/Compiler.pm
Criterion Covered Total %
statement 86 88 97.7
branch 59 88 67.0
condition 5 15 33.3
subroutine 10 10 100.0
pod 0 2 0.0
total 160 203 78.8


line stmt bran cond sub pod time code
1             package Sim::Agent::Compiler;
2              
3 3     3   1029 use strict;
  3         8  
  3         113  
4 3     3   16 use warnings;
  3         9  
  3         4919  
5              
6             sub new
7             {
8 2     2 0 47 my ($class) = @_;
9 2         37 bless {}, $class;
10             }
11              
12             sub compile
13             {
14 2     2 0 5 my ($self, $ast) = @_;
15              
16 2 50 33     24 die "Root must be (system ...)"
      33        
17             unless ref($ast) eq 'ARRAY'
18             && @$ast
19             && $ast->[0] eq 'system';
20              
21 2         12 my $graph =
22             {
23             entry => undef,
24             chief => undef,
25             limits => _default_limits(),
26             agents => {},
27             };
28              
29             # Process system body
30 2         11 for my $form (@$ast[1 .. $#$ast])
31             {
32              
33 14 50       39 die "Invalid form in system"
34             unless ref($form) eq 'ARRAY';
35              
36 14         51 my $keyword = $form->[0];
37              
38 14 100       51 if ($keyword eq 'limits')
    100          
    50          
39             {
40 1         4 _compile_limits($graph, $form);
41             }
42             elsif ($keyword eq 'entry')
43             {
44 2         11 _compile_entry($graph, $form);
45             }
46             elsif ($keyword eq 'agent')
47             {
48 11         24 _compile_agent($graph, $form);
49             }
50             else
51             {
52 0         0 die "Unknown system keyword: $keyword";
53             }
54             }
55              
56 2         11 _finalize_graph($graph);
57              
58 2         7 return $graph;
59             }
60              
61             # -------------------------------------------------------------------
62             # Internal Helpers (Private)
63             # -------------------------------------------------------------------
64              
65             sub _default_limits
66             {
67             return
68             {
69 2     2   22 max_depth => 5,
70             max_agents => 50,
71             max_iterations => 100,
72             max_self_cycles => 5,
73             max_pingpong => 4,
74             };
75             }
76              
77             sub _compile_limits
78             {
79 1     1   3 my ($graph, $form) = @_;
80              
81 1         4 for my $item (@$form[1 .. $#$form])
82             {
83              
84 3 50 33     16 die "Invalid limit entry"
85             unless ref($item) eq 'ARRAY'
86             && @$item == 2;
87              
88 3         8 my ($key, $value) = @$item;
89              
90             die "Unknown limit: $key"
91 3 50       10 unless exists $graph->{limits}->{$key};
92              
93 3 50 33     21 die "Limit must be integer"
94             unless defined $value && $value =~ /^\d+$/;
95              
96 3         11 $graph->{limits}->{$key} = int($value);
97             }
98             }
99              
100             sub _compile_entry
101             {
102 2     2   5 my ($graph, $form) = @_;
103              
104 2 50       9 die "Entry form must be (entry AgentName)"
105             unless @$form == 2;
106              
107             die "Multiple entry declarations"
108 2 50       8 if defined $graph->{entry};
109              
110 2         7 $graph->{entry} = $form->[1];
111             }
112              
113             sub _compile_agent
114             {
115 11     11   21 my ($graph, $form) = @_;
116              
117 11 50       30 die "Agent form too short"
118             unless @$form >= 3;
119              
120 11         18 my $name = $form->[1];
121              
122             die "Duplicate agent: $name"
123 11 50       32 if exists $graph->{agents}->{$name};
124              
125 11         75 my $agent =
126             {
127             name => $name,
128             role => undef,
129             model => undef,
130             prompt_file => undef,
131             selfcrit_file => undef,
132             revision_file => undef,
133             parse_file => undef,
134             send_to => undef,
135             on_ok => [],
136             on_fail => [],
137             wait_for => [],
138             };
139              
140 11         32 for my $attr (@$form[2 .. $#$form]) {
141              
142 51 50       113 die "Invalid agent attribute"
143             unless ref($attr) eq 'ARRAY';
144              
145 51         91 my $key = $attr->[0];
146              
147 51 100       221 if ($key eq 'role')
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
148             {
149 11 50       27 die "Role form invalid" unless @$attr == 2;
150 11         32 $agent->{role} = $attr->[1];
151             }
152             elsif ($key eq 'model')
153             {
154 5 50       14 die "Model form invalid" unless @$attr == 2;
155 5         13 $agent->{model} = $attr->[1];
156             }
157             elsif ($key eq 'prompt-file')
158             {
159 5 50       16 die "prompt-file invalid" unless @$attr == 2;
160 5         13 $agent->{prompt_file} = $attr->[1];
161             }
162             elsif ($key eq 'self-critic-file')
163             {
164 5 50       14 die "self-critic-file invalid" unless @$attr == 2;
165 5         13 $agent->{selfcrit_file} = $attr->[1];
166             }
167             elsif ($key eq 'revision-file')
168             {
169 5 50       20 die "revision-file invalid" unless @$attr == 2;
170 5         14 $agent->{revision_file} = $attr->[1];
171             }
172             elsif ($key eq 'parse-file')
173             {
174 6 50       22 die "parse-file invalid" unless @$attr == 2;
175 6         14 $agent->{parse_file} = $attr->[1];
176             }
177             elsif ($key eq 'send-to')
178             {
179 5 50       28 die "send-to must have exactly one target"
180             unless @$attr == 2;
181 5         15 $agent->{send_to} = $attr->[1];
182             }
183             elsif ($key eq 'on-ok')
184             {
185 4         32 $agent->{on_ok} = _extract_list($attr);
186             }
187             elsif ($key eq 'on-fail')
188             {
189 4         10 $agent->{on_fail} = _extract_list($attr);
190             }
191             elsif ($key eq 'wait-for')
192             {
193 1         2 $agent->{wait_for} = _extract_list($attr);
194             }
195             else
196             {
197 0         0 die "Unknown agent attribute: $key";
198             }
199             }
200              
201             die "Agent $name missing role"
202 11 50       31 unless $agent->{role};
203              
204 11         37 $graph->{agents}->{$name} = $agent;
205             }
206              
207             sub _extract_list
208             {
209 9     9   18 my ($form) = @_;
210              
211 9 50 33     59 die "Expected list form"
212             unless @$form == 2
213             && ref($form->[1]) eq 'ARRAY';
214              
215 9         24 return $form->[1];
216             }
217              
218             sub _finalize_graph
219             {
220 2     2   4 my ($graph) = @_;
221              
222             die "Missing entry"
223 2 50       31 unless defined $graph->{entry};
224              
225             die "Entry agent not defined"
226 2 50       10 unless exists $graph->{agents}->{ $graph->{entry} };
227              
228 2         5 my $chief_count = 0;
229              
230 2         6 for my $agent (values %{ $graph->{agents} }) {
  2         9  
231              
232             # Count chief
233 11 100       29 if ($agent->{role} eq 'chief')
234             {
235 2         4 $chief_count++;
236 2         14 $graph->{chief} = $agent->{name};
237             }
238              
239             die "Invalid role: $agent->{role}"
240 11 50       58 unless $agent->{role} =~ /^(worker|critic|chief)$/;
241              
242 11 100       28 if ($agent->{role} eq 'worker')
243             {
244             die "Worker $agent->{name} missing send-to"
245 5 50       23 unless defined $agent->{send_to};
246             }
247              
248 11 100       55 if ($agent->{role} eq 'critic')
249             {
250             die "Critic $agent->{name} missing on-ok"
251 4 50       8 unless @{ $agent->{on_ok} };
  4         12  
252             die "Critic $agent->{name} missing on-fail"
253 4 50       7 unless @{ $agent->{on_fail} };
  4         12  
254             }
255              
256 11 100       21 for my $target (
257 11         23 @{ $agent->{on_ok} },
258 11         21 @{ $agent->{on_fail} },
259 11         33 @{ $agent->{wait_for} },
260             (defined $agent->{send_to} ? ($agent->{send_to}) : ())
261             )
262             {
263             die "Undefined agent reference: $target"
264 16 50       47 unless exists $graph->{agents}->{$target};
265             }
266             }
267              
268 2 50       10 die "Exactly one chief required"
269             unless $chief_count == 1;
270             }
271              
272             1;
273              
274             =pod
275              
276             =head1 NAME
277              
278             Sim::Agent::Compiler - Compiles the S-expression AST into a validated execution graph
279              
280             =head1 DESCRIPTION
281              
282             Takes the parsed AST and produces a normalized graph structure: limits, agents, routing, and reference validation.
283              
284             See L for DSL details.
285              
286             =head1 AUTHOR
287              
288             Gian Luca Brunetti (2026), gianluca.brunetti@gmail.com
289              
290             =head1 LICENSE
291              
292             The GNU General Public License v3.0
293              
294             =cut
295              
296