Skip to content

Commit 4eaf290

Browse files
committed
Introduce LoopTools for tool and toolkit singleton registration.
1 parent f88202c commit 4eaf290

File tree

5 files changed

+185
-96
lines changed

5 files changed

+185
-96
lines changed

src/Loop.php

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Facades\Auth;
7-
use Kirschbaum\Loop\Collections\ToolCollection;
87
use Kirschbaum\Loop\Contracts\Tool;
98
use Kirschbaum\Loop\Contracts\Toolkit;
109
use Prism\Prism\Enums\Provider;
@@ -16,16 +15,9 @@
1615

1716
class Loop
1817
{
19-
protected ToolCollection $tools;
20-
2118
protected string $context = '';
2219

23-
protected bool $hasBeenResolved = false;
24-
25-
public function __construct()
26-
{
27-
$this->tools = new ToolCollection;
28-
}
20+
public function __construct(protected LoopTools $loopTools) {}
2921

3022
public function setup(): void {}
3123

@@ -38,26 +30,14 @@ public function context(string $context): static
3830

3931
public function tool(Tool $tool): static
4032
{
41-
$this->tools->push($tool);
42-
43-
$this->storeRegistration(function (Loop $loop) use ($tool) {
44-
$loop->tools->push($tool);
45-
});
33+
$this->loopTools->registerTool($tool);
4634

4735
return $this;
4836
}
4937

5038
public function toolkit(Toolkit $toolkit): static
5139
{
52-
foreach ($toolkit->getTools() as $tool) {
53-
$this->tools->push($tool);
54-
}
55-
56-
$this->storeRegistration(function (Loop $loop) use ($toolkit) {
57-
foreach ($toolkit->getTools() as $tool) {
58-
$loop->tools->push($tool);
59-
}
60-
});
40+
$this->loopTools->registerToolkit($toolkit);
6141

6242
return $this;
6343
}
@@ -109,39 +89,22 @@ public function ask(string $question, Collection $messages): Response
10989
->asText();
11090
}
11191

92+
/**
93+
* @return Collection<array-key, PrismTool>
94+
*/
11295
public function getPrismTools(): Collection
11396
{
114-
return $this->tools
97+
return $this->loopTools
98+
->getTools()
11599
->toBase()
116100
->map(fn (Tool $tool) => $tool->build());
117101
}
118102

119103
public function getPrismTool(string $name): PrismTool
120104
{
121-
return $this->tools->getTool($name)->build();
122-
}
123-
124-
/**
125-
* Store a registration callback for replay in new instances.
126-
*/
127-
protected function storeRegistration(callable $callback): void
128-
{
129-
if (! app()->bound('loop.registrations')) {
130-
app()->instance('loop.registrations', []);
131-
}
132-
133-
$registrations = app('loop.registrations');
134-
$registrations[] = $callback;
135-
app()->instance('loop.registrations', $registrations);
136-
}
137-
138-
public function markAsResolved(): void
139-
{
140-
$this->hasBeenResolved = true;
141-
}
142-
143-
public function hasBeenResolved(): bool
144-
{
145-
return $this->hasBeenResolved;
105+
return $this->loopTools
106+
->getTools()
107+
->getTool($name)
108+
->build();
146109
}
147110
}

src/LoopServiceProvider.php

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,17 @@ public function configurePackage(Package $package): void
3535

3636
public function packageBooted(): void
3737
{
38+
$this->app->singleton(LoopTools::class, function () {
39+
return new LoopTools;
40+
});
41+
3842
$this->app->scoped(Loop::class, function ($app) {
39-
$loop = new Loop;
43+
$loop = new Loop($app->make(LoopTools::class));
4044
$loop->setup();
4145

4246
return $loop;
4347
});
4448

45-
/**
46-
* Set up container resolving hook to replay tool registrations for Octane compatibility
47-
*/
48-
$this->app->resolving(Loop::class, function ($loop, $app) {
49-
$loop->markAsResolved();
50-
51-
if ($app->bound('loop.registrations')) {
52-
$registrations = $app->make('loop.registrations');
53-
if (is_array($registrations)) {
54-
foreach ($registrations as $registration) {
55-
if (is_callable($registration)) {
56-
$registration($loop);
57-
}
58-
}
59-
}
60-
}
61-
});
62-
6349
$this->app->bind(SseService::class, function ($app) {
6450
return new SseService;
6551
});

src/LoopTools.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Kirschbaum\Loop;
4+
5+
use Kirschbaum\Loop\Collections\ToolCollection;
6+
use Kirschbaum\Loop\Contracts\Tool;
7+
use Kirschbaum\Loop\Contracts\Toolkit;
8+
9+
class LoopTools
10+
{
11+
protected ToolCollection $tools;
12+
13+
/** @var array<int, Toolkit> */
14+
protected array $toolkits = [];
15+
16+
public function __construct()
17+
{
18+
$this->tools = new ToolCollection;
19+
}
20+
21+
public function registerTool(Tool $tool): void
22+
{
23+
$this->tools->push($tool);
24+
}
25+
26+
public function registerToolkit(Toolkit $toolkit): void
27+
{
28+
$this->toolkits[] = $toolkit;
29+
30+
foreach ($toolkit->getTools() as $tool) {
31+
$this->registerTool($tool);
32+
}
33+
}
34+
35+
/**
36+
* Get all registered tools
37+
*/
38+
public function getTools(): ToolCollection
39+
{
40+
return $this->tools;
41+
}
42+
43+
/**
44+
* Get all registered toolkits
45+
*
46+
* @return array<int, Toolkit>
47+
*/
48+
public function getToolkits(): array
49+
{
50+
return $this->toolkits;
51+
}
52+
53+
/**
54+
* Clear all registrations
55+
*/
56+
public function clear(): void
57+
{
58+
$this->tools = new ToolCollection;
59+
$this->toolkits = [];
60+
}
61+
}

tests/Feature/LoopToolsTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Kirschbaum\Loop\Facades\Loop as LoopFacade;
6+
use Kirschbaum\Loop\Loop;
7+
use Kirschbaum\Loop\LoopTools;
8+
use Kirschbaum\Loop\Toolkits\LaravelModelToolkit;
9+
use Kirschbaum\Loop\Tools\CustomTool;
10+
use Workbench\App\Models\User;
11+
12+
beforeEach(function () {
13+
LoopFacade::clearResolvedInstances();
14+
15+
app()->forgetInstance(Loop::class);
16+
17+
if (app()->bound(LoopTools::class)) {
18+
app(LoopTools::class)->clear();
19+
}
20+
});
21+
22+
test('tools persist across loop instances', function () {
23+
$tool = CustomTool::make('test_tool', 'A test tool')
24+
->using(fn () => 'Test tool response');
25+
26+
LoopFacade::tool($tool);
27+
28+
$loop1 = app(Loop::class);
29+
$tools1 = $loop1->getPrismTools();
30+
31+
expect($tools1)
32+
->toHaveCount(1)
33+
->and($tools1->first()->name())
34+
->toEqual('test_tool');
35+
36+
app()->forgetInstance(Loop::class);
37+
38+
$loop2 = app(Loop::class);
39+
$tools2 = $loop2->getPrismTools();
40+
41+
expect($tools2)
42+
->toHaveCount(1)
43+
->and($tools2->first()->name())
44+
->toEqual('test_tool');
45+
});
46+
47+
test('toolkit registrations persist across loop instances', function () {
48+
LoopFacade::toolkit(LaravelModelToolkit::make([User::class]));
49+
50+
$loop1 = app(Loop::class);
51+
$tools1 = $loop1->getPrismTools();
52+
53+
expect($tools1->count())
54+
->toBeGreaterThan(0);
55+
56+
app()->forgetInstance(Loop::class);
57+
58+
$loop2 = app(Loop::class);
59+
$tools2 = $loop2->getPrismTools();
60+
61+
expect($tools2->count())
62+
->toEqual($tools1->count());
63+
});
64+
65+
test('multiple tool registrations persist', function () {
66+
LoopFacade::tool(CustomTool::make('tool1', 'Tool 1')->using(fn () => 'Response 1'));
67+
LoopFacade::tool(CustomTool::make('tool2', 'Tool 2')->using(fn () => 'Response 2'));
68+
69+
$loop1 = app(Loop::class);
70+
71+
expect($loop1->getPrismTools())
72+
->toHaveCount(2);
73+
74+
app()->forgetInstance(Loop::class);
75+
76+
$loop2 = app(Loop::class);
77+
78+
expect($loop2->getPrismTools())
79+
->toHaveCount(2);
80+
});
81+
82+
test('loop tools registry is a singleton', function () {
83+
$registry1 = app(LoopTools::class);
84+
$registry2 = app(LoopTools::class);
85+
86+
expect($registry1)
87+
->toBe($registry2);
88+
});
Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
use Kirschbaum\Loop\Facades\Loop as LoopFacade;
46
use Kirschbaum\Loop\Loop;
7+
use Kirschbaum\Loop\LoopTools;
58
use Kirschbaum\Loop\Toolkits\LaravelModelToolkit;
69
use Workbench\App\Models\User;
710

811
beforeEach(function () {
12+
// Simulate Octane worker reset by clearing resolved instances and stored tools
913
app()->forgetInstance(Loop::class);
10-
app()->forgetInstance('loop.registrations');
14+
15+
if (app()->bound(LoopTools::class)) {
16+
app(LoopTools::class)->clear();
17+
}
1118
});
1219

1320
test('toolkits persist after instance recreation', function () {
@@ -17,56 +24,40 @@
1724
$firstTools = $firstInstance->getPrismTools();
1825

1926
expect($firstTools->count())
20-
->toBeGreaterThan(0, 'First instance should have tools from registered toolkit');
27+
->toBeGreaterThan(0);
2128

22-
app()->forgetInstance(Loop::class); // Simulates Octane instance recreation.
29+
app()->forgetInstance(Loop::class); // Simulate Octane instance recreation.
2330

2431
$secondInstance = app(Loop::class);
2532
$secondTools = $secondInstance->getPrismTools();
2633

2734
expect($secondTools->count())
28-
->toBeGreaterThan(0, 'Second instance should have restored tools from registrations')
29-
->and($secondTools->count())
30-
->toEqual($firstTools->count(), 'Both instances should have the same number of tools');
35+
->toEqual($firstTools->count());
3136
});
3237

3338
test('multiple toolkit registrations are preserved', function () {
3439
LoopFacade::toolkit(LaravelModelToolkit::make([User::class]));
3540
LoopFacade::toolkit(LaravelModelToolkit::make([User::class]));
3641

3742
$firstInstance = app(Loop::class);
38-
$firstTools = $firstInstance->getPrismTools();
39-
$firstCount = $firstTools->count();
43+
$firstCount = $firstInstance->getPrismTools()->count();
4044

41-
expect($firstCount)->toBeGreaterThan(0, 'Should have tools from multiple registrations');
45+
expect($firstCount)
46+
->toBeGreaterThan(0);
4247

4348
app()->forgetInstance(Loop::class);
4449

4550
$secondInstance = app(Loop::class);
46-
$secondTools = $secondInstance->getPrismTools();
51+
$secondCount = $secondInstance->getPrismTools()->count();
4752

48-
expect($secondTools->count())->toEqual($firstCount, 'All toolkit registrations should be preserved');
53+
expect($secondCount)
54+
->toEqual($firstCount);
4955
});
5056

5157
test('empty registrations handle gracefully', function () {
5258
$instance = app(Loop::class);
5359
$tools = $instance->getPrismTools();
5460

55-
expect($tools->count())->toEqual(0, 'Should have no tools when nothing is registered');
56-
});
57-
58-
test('registrations are stored correctly', function () {
59-
LoopFacade::toolkit(LaravelModelToolkit::make([User::class]));
60-
61-
expect(app()->bound('loop.registrations'))->toBeTrue('Registrations should be stored in container');
62-
63-
$registrations = app('loop.registrations');
64-
expect($registrations)
65-
->toBeArray('Registrations should be an array')
66-
->and(count($registrations))
67-
->toBeGreaterThan(0, 'Should have at least one registration');
68-
69-
foreach ($registrations as $registration) {
70-
expect(is_callable($registration))->toBeTrue('Each registration should be callable');
71-
}
61+
expect($tools)
62+
->toHaveCount(0);
7263
});

0 commit comments

Comments
 (0)