File Coverage

blib/lib/Sim/AgentSoar/AgentSoar.pm
Criterion Covered Total %
statement 12 78 15.3
branch 0 28 0.0
condition 0 10 0.0
subroutine 4 9 44.4
pod 3 3 100.0
total 19 128 14.8


line stmt bran cond sub pod time code
1             package Sim::AgentSoar::AgentSoar;
2              
3 1     1   7 use strict;
  1         2  
  1         43  
4 1     1   6 use warnings;
  1         2  
  1         73  
5              
6             our $VERSION = '0.06';
7              
8 1     1   548 use Sim::AgentSoar::Engine;
  1         3  
  1         41  
9 1     1   527 use Sim::AgentSoar::Node;
  1         3  
  1         1149  
10              
11             sub new
12             {
13 0     0 1   my ($class, %args) = @_;
14              
15             my $self =
16             {
17             worker => $args{worker},
18             max_depth => $args{max_depth} // 20,
19             branching_factor => $args{branching_factor} // 1,
20             regression_tolerance => $args{regression_tolerance},
21              
22 0   0       stats =>
      0        
23             {
24             nodes_expanded => 0,
25             children_created => 0,
26             llm_calls => 0,
27             corrections => 0,
28             },
29             };
30              
31 0 0         die "worker required" unless $self->{worker};
32              
33 0           return bless $self, $class;
34             }
35              
36             sub run
37             {
38 0     0 1   my ($self, %args) = @_;
39              
40 0           my $start = $args{start};
41 0           my $target = $args{target};
42              
43 0 0         die "start required" unless defined $start;
44 0 0         die "target required" unless defined $target;
45              
46 0           my @OPEN;
47             my %VISITED;
48 0           my %NODES;
49 0           my $node_id = 0;
50              
51 0           my $root = Sim::AgentSoar::Node->new(
52             id => $node_id,
53             parent => undef,
54             value => $start,
55             metric => Sim::AgentSoar::Engine->metric($start, $target),
56             depth => 0,
57             operator => undef,
58             );
59              
60 0           push @OPEN, $root;
61 0           $VISITED{$start} = 1;
62 0           $NODES{$node_id} = $root;
63 0           $node_id++;
64              
65 0           while (@OPEN) {
66              
67             # Best-first by (metric, depth)
68             @OPEN = sort
69             {
70 0 0         $a->metric <=> $b->metric
  0            
71             ||
72             $a->depth <=> $b->depth
73             } @OPEN;
74              
75 0           my $node = shift @OPEN;
76 0           $self->{stats}->{nodes_expanded}++;
77              
78 0 0         if ($node->metric == 0)
79             {
80 0           return $self->_reconstruct_path($node, \%NODES);
81             }
82              
83 0 0         next if $node->depth >= $self->{max_depth};
84              
85 0           my @children =
86             $self->_expand_node($node, $target, \%VISITED, \$node_id);
87            
88 0           $self->{stats}->{children_created} += scalar @children;
89 0           push @OPEN, @children;
90             }
91              
92 0           return undef;
93             }
94              
95             sub _expand_node
96             {
97 0     0     my ($self, $node, $target, $visited, $node_id_ref) = @_;
98              
99 0           my @children;
100             my %local_proposals;
101 0           my $attempts = 0;
102 0           my $max_attempts = 10;
103              
104 0   0       while (
105             @children < $self->{branching_factor}
106             && $attempts < $max_attempts
107             )
108             {
109              
110 0           $self->{stats}->{llm_calls}++;
111             my $operator = $self->{worker}->propose(
112 0           value => $node->value,
113             metric => $node->metric,
114             target => $target,
115             );
116              
117 0           $attempts++;
118              
119 0 0         next unless defined $operator;
120 0 0         next if $local_proposals{$operator}++;
121              
122 0           my $new_value =
123             Sim::AgentSoar::Engine->apply_operator(
124             $node->value, $operator
125             );
126              
127 0 0         next unless Sim::AgentSoar::Engine->valid_value($new_value);
128              
129 0           my $new_metric =
130             Sim::AgentSoar::Engine->metric($new_value, $target);
131              
132             # ---- Correction stage ----
133 0 0         if (defined $self->{regression_tolerance})
134             {
135            
136 0           $self->{stats}->{llm_calls}++;
137 0           $self->{stats}->{corrections}++;
138              
139             my $corrected =
140             $self->{worker}->correct(
141             value => $node->value,
142             target => $target,
143             operator => $operator,
144             old_metric => $node->metric,
145             new_metric => $new_metric,
146             regression_tolerance => $self->{regression_tolerance},
147 0           );
148              
149 0 0 0       if (defined $corrected && $corrected ne $operator)
150             {
151              
152 0           $operator = $corrected;
153              
154 0           $new_value =
155             Sim::AgentSoar::Engine->apply_operator(
156             $node->value, $operator
157             );
158              
159 0 0         next unless Sim::AgentSoar::Engine->valid_value($new_value);
160              
161 0           $new_metric =
162             Sim::AgentSoar::Engine->metric($new_value, $target);
163             }
164             }
165             # ---- End correction ----
166              
167 0 0         next if $visited->{$new_value};
168              
169 0           my $child = Sim::AgentSoar::Node->new(
170             id => $$node_id_ref,
171             parent => $node->id,
172             value => $new_value,
173             metric => $new_metric,
174             depth => $node->depth + 1,
175             operator => $operator,
176             );
177              
178 0           $visited->{$new_value} = 1;
179 0           $$node_id_ref++;
180              
181 0           push @children, $child;
182             }
183              
184 0           return @children;
185             }
186              
187             sub _reconstruct_path
188             {
189 0     0     my ($self, $node, $nodes) = @_;
190              
191 0           my @path;
192              
193 0           while ($node)
194             {
195 0           unshift @path,
196             {
197             value => $node->value,
198             operator => $node->operator,
199             };
200              
201 0           my $parent_id = $node->parent;
202 0 0         $node = defined $parent_id ? $nodes->{$parent_id} : undef;
203             }
204              
205 0           return \@path;
206             }
207              
208             sub stats
209             {
210 0     0 1   my ($self) = @_;
211 0           return $self->{stats};
212             }
213              
214             1;
215              
216              
217             =pod
218              
219             =head1 NAME
220              
221             Sim::AgentSoar::AgentSoar - Explicit SOAR-inspired search controller with LLM-guided operator selection
222              
223             =head1 SYNOPSIS
224              
225             use Sim::AgentSoar::AgentSoar;
226             use Sim::AgentSoar::Worker;
227              
228             my $worker = Sim::AgentSoar::Worker->new(
229             model => 'llama3.2:1b',
230             );
231              
232             my $search = Sim::AgentSoar::AgentSoar->new(
233             worker => $worker,
234             branching_factor => 2,
235             regression_tolerance => 2,
236             max_depth => 20,
237             );
238              
239             my $path = $search->run(
240             start => 4,
241             target => 19,
242             );
243              
244             =head1 DESCRIPTION
245              
246             Sim::AgentSoar::AgentSoar implements an explicit state-space search architecture
247             inspired by the classical SOAR cognitive model, but reinterpreted with modern
248             LLM-assisted heuristic control. Indeed, the model substitute the flat, uplfront
249             planning mode of LLMs with a Soar-like Subgoalling approach.
250             The last Lisp implementation, Soar 5.0, has been taken as a reference.
251              
252             The architecture strictly separates:
253              
254             =over 4
255              
256             =item * Structural recursion (search tree expansion)
257              
258             =item * Deterministic evaluation (Engine)
259              
260             =item * Heuristic proposal (Worker / LLM)
261              
262             =item * Optional bounded local correction
263              
264             =back
265              
266             Unlike purely LLM-driven agents, this module preserves deterministic
267             control over state validation, search ordering, and termination criteria.
268             The LLM is never allowed to:
269              
270             =over 4
271              
272             =item * Evaluate goal satisfaction
273              
274             =item * Modify search ordering
275              
276             =item * Override state validity
277              
278             =item * Introduce nondeterministic structural changes
279              
280             =back
281              
282             This guarantees that heuristic instability cannot corrupt the search backbone.
283              
284             =head2 AgentSoar Model
285              
286             The search procedure maintains:
287              
288             =over 4
289              
290             =item * An OPEN list ordered by (metric, depth)
291              
292             =item * A VISITED set preventing state repetition
293              
294             =item * A bounded branching factor
295              
296             =item * Optional regression tolerance logic
297              
298             =back
299              
300             Each node expansion proceeds as follows:
301              
302             =over 4
303              
304             =item 1. The Worker proposes an operator.
305              
306             =item 2. The Engine deterministically applies and evaluates the operator.
307              
308             =item 3. If regression exceeds tolerance, a single correction pass is allowed.
309              
310             =item 4. Valid child nodes are inserted into OPEN.
311              
312             =back
313              
314             The recursion is structural (tree-based), not narrative.
315              
316             =head2 Instrumentation
317              
318             The module records runtime statistics accessible via C:
319              
320             {
321             nodes_expanded => ...,
322             children_created => ...,
323             llm_calls => ...,
324             corrections => ...,
325             }
326              
327             This supports empirical evaluation of heuristic efficiency.
328              
329             =head1 CONSTRUCTOR
330              
331             =head2 new
332              
333             my $search = Sim::AgentSoar::AgentSoar->new(
334             worker => $worker, # required
335             branching_factor => 2, # default 1
336             regression_tolerance => 2, # optional
337             max_depth => 20, # default 20
338             );
339              
340             =head1 METHODS
341              
342             =head2 run
343              
344             my $path = $search->run(
345             start => $start,
346             target => $target,
347             );
348              
349             Executes the search and returns an array reference describing the solution
350             path, or undef if no solution is found within constraints.
351              
352             =head2 stats
353              
354             Returns a hash reference of runtime statistics.
355              
356             =head1 RESEARCH NOTES
357              
358             This implementation explores a hybrid model:
359              
360             =over 4
361              
362             =item * Explicit symbolic search
363              
364             =item * LLM-guided operator selection
365              
366             =item * Bounded internal recursion (correction stage)
367              
368             =item * Deterministic invariants
369              
370             =back
371              
372             The design intentionally avoids letting the LLM control topology.
373             All structural evolution must occur through explicit, measurable mechanisms.
374              
375             =head1 AUTHOR
376              
377             Gian Luca Brunetti (2026), gianluca.brunetti@gmail.com
378              
379             AI tools were used to accelerate drafting and refactoring. No changes were merged without human review; the maintainer remains the sole accountable party for correctness, security, and licensing compliance.
380              
381             =cut