From 0be3ff2ab6a6b7ad85769334abb361f599319fa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fernando=20S=C3=A1nchez?=
 <34519027+fsmonter@users.noreply.github.com>
Date: Thu, 12 Jun 2025 18:15:13 +0100
Subject: [PATCH] Refactor Loop class to use a LoopTools singleton for tool
 management

---
 src/Loop.php                    | 22 ++++-----
 src/LoopServiceProvider.php     |  6 ++-
 src/LoopTools.php               | 61 +++++++++++++++++++++++
 tests/Feature/LoopToolsTest.php | 88 +++++++++++++++++++++++++++++++++
 4 files changed, 163 insertions(+), 14 deletions(-)
 create mode 100644 src/LoopTools.php
 create mode 100644 tests/Feature/LoopToolsTest.php

diff --git a/src/Loop.php b/src/Loop.php
index 5521a3a..50ccb78 100644
--- a/src/Loop.php
+++ b/src/Loop.php
@@ -4,7 +4,6 @@
 
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Auth;
-use Kirschbaum\Loop\Collections\ToolCollection;
 use Kirschbaum\Loop\Contracts\Tool;
 use Kirschbaum\Loop\Contracts\Toolkit;
 use Prism\Prism\Enums\Provider;
@@ -16,14 +15,9 @@
 
 class Loop
 {
-    protected ToolCollection $tools;
-
     protected string $context = '';
 
-    public function __construct()
-    {
-        $this->tools = new ToolCollection;
-    }
+    public function __construct(protected LoopTools $loopTools) {}
 
     public function setup(): void {}
 
@@ -36,16 +30,14 @@ public function context(string $context): static
 
     public function tool(Tool $tool): static
     {
-        $this->tools->push($tool);
+        $this->loopTools->registerTool($tool);
 
         return $this;
     }
 
     public function toolkit(Toolkit $toolkit): static
     {
-        foreach ($toolkit->getTools() as $tool) {
-            $this->tool($tool);
-        }
+        $this->loopTools->registerToolkit($toolkit);
 
         return $this;
     }
@@ -99,13 +91,17 @@ public function ask(string $question, Collection $messages): Response
 
     public function getPrismTools(): Collection
     {
-        return $this->tools
+        return $this->loopTools
+            ->getTools()
             ->toBase()
             ->map(fn (Tool $tool) => $tool->build());
     }
 
     public function getPrismTool(string $name): PrismTool
     {
-        return $this->tools->getTool($name)->build();
+        return $this->loopTools
+            ->getTools()
+            ->getTool($name)
+            ->build();
     }
 }
diff --git a/src/LoopServiceProvider.php b/src/LoopServiceProvider.php
index 811a34e..5f14abe 100644
--- a/src/LoopServiceProvider.php
+++ b/src/LoopServiceProvider.php
@@ -35,8 +35,12 @@ public function configurePackage(Package $package): void
 
     public function packageBooted(): void
     {
+        $this->app->singleton(LoopTools::class, function () {
+            return new LoopTools;
+        });
+
         $this->app->scoped(Loop::class, function ($app) {
-            $loop = new Loop;
+            $loop = new Loop($app->make(LoopTools::class));
             $loop->setup();
 
             return $loop;
diff --git a/src/LoopTools.php b/src/LoopTools.php
new file mode 100644
index 0000000..7b5e55b
--- /dev/null
+++ b/src/LoopTools.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kirschbaum\Loop;
+
+use Kirschbaum\Loop\Collections\ToolCollection;
+use Kirschbaum\Loop\Contracts\Tool;
+use Kirschbaum\Loop\Contracts\Toolkit;
+
+class LoopTools
+{
+    protected ToolCollection $tools;
+
+    /** @var array<int, Toolkit> */
+    protected array $toolkits = [];
+
+    public function __construct()
+    {
+        $this->tools = new ToolCollection;
+    }
+
+    public function registerTool(Tool $tool): void
+    {
+        $this->tools->push($tool);
+    }
+
+    public function registerToolkit(Toolkit $toolkit): void
+    {
+        $this->toolkits[] = $toolkit;
+
+        foreach ($toolkit->getTools() as $tool) {
+            $this->registerTool($tool);
+        }
+    }
+
+    /**
+     * Get all registered tools
+     */
+    public function getTools(): ToolCollection
+    {
+        return $this->tools;
+    }
+
+    /**
+     * Get all registered toolkits
+     *
+     * @return array<int, Toolkit>
+     */
+    public function getToolkits(): array
+    {
+        return $this->toolkits;
+    }
+
+    /**
+     * Clear all registrations
+     */
+    public function clear(): void
+    {
+        $this->tools = new ToolCollection;
+        $this->toolkits = [];
+    }
+}
diff --git a/tests/Feature/LoopToolsTest.php b/tests/Feature/LoopToolsTest.php
new file mode 100644
index 0000000..4296b3c
--- /dev/null
+++ b/tests/Feature/LoopToolsTest.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+use Kirschbaum\Loop\Facades\Loop as LoopFacade;
+use Kirschbaum\Loop\Loop;
+use Kirschbaum\Loop\LoopTools;
+use Kirschbaum\Loop\Toolkits\LaravelModelToolkit;
+use Kirschbaum\Loop\Tools\CustomTool;
+use Workbench\App\Models\User;
+
+beforeEach(function () {
+    LoopFacade::clearResolvedInstances();
+
+    app()->forgetInstance(Loop::class);
+
+    if (app()->bound(LoopTools::class)) {
+        app(LoopTools::class)->clear();
+    }
+});
+
+test('tools persist across loop instances', function () {
+    $tool = CustomTool::make('test_tool', 'A test tool')
+        ->using(fn () => 'Test tool response');
+
+    LoopFacade::tool($tool);
+
+    $loop1 = app(Loop::class);
+    $tools1 = $loop1->getPrismTools();
+
+    expect($tools1)
+        ->toHaveCount(1)
+        ->and($tools1->first()->name())
+        ->toEqual('test_tool');
+
+    app()->forgetInstance(Loop::class);
+
+    $loop2 = app(Loop::class);
+    $tools2 = $loop2->getPrismTools();
+
+    expect($tools2)
+        ->toHaveCount(1)
+        ->and($tools2->first()->name())
+        ->toEqual('test_tool');
+});
+
+test('toolkit registrations persist across loop instances', function () {
+    LoopFacade::toolkit(LaravelModelToolkit::make([User::class]));
+
+    $loop1 = app(Loop::class);
+    $tools1 = $loop1->getPrismTools();
+
+    expect($tools1->count())
+        ->toBeGreaterThan(0);
+
+    app()->forgetInstance(Loop::class);
+
+    $loop2 = app(Loop::class);
+    $tools2 = $loop2->getPrismTools();
+
+    expect($tools2->count())
+        ->toEqual($tools1->count());
+});
+
+test('multiple tool registrations persist', function () {
+    LoopFacade::tool(CustomTool::make('tool1', 'Tool 1')->using(fn () => 'Response 1'));
+    LoopFacade::tool(CustomTool::make('tool2', 'Tool 2')->using(fn () => 'Response 2'));
+
+    $loop1 = app(Loop::class);
+
+    expect($loop1->getPrismTools())
+        ->toHaveCount(2);
+
+    app()->forgetInstance(Loop::class);
+
+    $loop2 = app(Loop::class);
+
+    expect($loop2->getPrismTools())
+        ->toHaveCount(2);
+});
+
+test('loop tools registry is a singleton', function () {
+    $registry1 = app(LoopTools::class);
+    $registry2 = app(LoopTools::class);
+
+    expect($registry1)
+        ->toBe($registry2);
+});