diff --git a/Makefile b/Makefile
index c7d6cf2145afce5aa33688512dfff601f8c18f1f..645d4b4f03a6185493916ce64bcb2f4014148eff 100644
--- a/Makefile
+++ b/Makefile
@@ -107,6 +107,7 @@ esbuild: $(CSS_SOURCES) node_modules
 		--loader:.ttf=file \
 		--loader:.webp=file \
 		--metafile=esbuild-meta-docs.json \
+		--minify \
 		--outdir=$(ESBUILD_TARGET_DIRECTORY) \
 		--sourcemap \
 		--splitting \
diff --git a/composer.json b/composer.json
index b4887bcebb5a42ee182306b01f839514c90719e1..afd5f890a5b9481b566d534f498992cd8db16c4f 100644
--- a/composer.json
+++ b/composer.json
@@ -4,6 +4,12 @@
     "type": "library",
     "license": "MIT",
     "autoload": {
+        "files": [
+            "src/helpers/coroutineMustGetContext.php",
+            "src/helpers/coroutineMustGo.php",
+            "src/helpers/coroutineMustRun.php",
+            "src/helpers/generatorGetReturn.php"
+        ],
         "psr-4": {
             "Distantmagic\\Docs\\": "app/",
             "Distantmagic\\Resonance\\": "src/"
diff --git a/composer.lock b/composer.lock
index 3f8829223a7187f58f947e9b477e52cc790fa031..081003b3c38e7b17230b1e3e6d44a83dfbfa6d85 100644
--- a/composer.lock
+++ b/composer.lock
@@ -344,16 +344,16 @@
         },
         {
             "name": "doctrine/collections",
-            "version": "2.2.1",
+            "version": "2.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/collections.git",
-                "reference": "420480fc085bc65f3c956af13abe8e7546f94813"
+                "reference": "d8af7f248c74f195f7347424600fd9e17b57af59"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/collections/zipball/420480fc085bc65f3c956af13abe8e7546f94813",
-                "reference": "420480fc085bc65f3c956af13abe8e7546f94813",
+                "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59",
+                "reference": "d8af7f248c74f195f7347424600fd9e17b57af59",
                 "shasum": ""
             },
             "require": {
@@ -410,7 +410,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/collections/issues",
-                "source": "https://github.com/doctrine/collections/tree/2.2.1"
+                "source": "https://github.com/doctrine/collections/tree/2.2.2"
             },
             "funding": [
                 {
@@ -426,7 +426,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-03-05T22:28:45+00:00"
+            "time": "2024-04-18T06:56:21+00:00"
         },
         {
             "name": "doctrine/dbal",
@@ -1016,16 +1016,16 @@
         },
         {
             "name": "doctrine/orm",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/orm.git",
-                "reference": "9c560713925ac5859342e6ff370c4c997acf2fd4"
+                "reference": "f79d166a4e844beb9389f23bdb44abdbf58cec38"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/orm/zipball/9c560713925ac5859342e6ff370c4c997acf2fd4",
-                "reference": "9c560713925ac5859342e6ff370c4c997acf2fd4",
+                "url": "https://api.github.com/repos/doctrine/orm/zipball/f79d166a4e844beb9389f23bdb44abdbf58cec38",
+                "reference": "f79d166a4e844beb9389f23bdb44abdbf58cec38",
                 "shasum": ""
             },
             "require": {
@@ -1098,9 +1098,9 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/orm/issues",
-                "source": "https://github.com/doctrine/orm/tree/3.1.1"
+                "source": "https://github.com/doctrine/orm/tree/3.1.2"
             },
-            "time": "2024-03-21T11:37:52+00:00"
+            "time": "2024-04-15T14:20:40+00:00"
         },
         {
             "name": "doctrine/persistence",
@@ -2057,16 +2057,16 @@
         },
         {
             "name": "hyperf/collection",
-            "version": "v3.1.15",
+            "version": "v3.1.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/collection.git",
-                "reference": "d0ac957987a704c8b2a16de4333b81f1f56a724a"
+                "reference": "fe9adae0fe88ec3330505c0cea758feee886d327"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/collection/zipball/d0ac957987a704c8b2a16de4333b81f1f56a724a",
-                "reference": "d0ac957987a704c8b2a16de4333b81f1f56a724a",
+                "url": "https://api.github.com/repos/hyperf/collection/zipball/fe9adae0fe88ec3330505c0cea758feee886d327",
+                "reference": "fe9adae0fe88ec3330505c0cea758feee886d327",
                 "shasum": ""
             },
             "require": {
@@ -2116,7 +2116,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-03-23T11:28:51+00:00"
+            "time": "2024-04-13T07:45:03+00:00"
         },
         {
             "name": "hyperf/conditionable",
@@ -2361,20 +2361,20 @@
         },
         {
             "name": "hyperf/engine",
-            "version": "v2.10.6",
+            "version": "v2.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/engine.git",
-                "reference": "5617f01e40980723512f510cbacfd9a4671aaa5b"
+                "reference": "26e0b65fc2a63a00266e7124e221c6f3fb2c8e95"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/engine/zipball/5617f01e40980723512f510cbacfd9a4671aaa5b",
-                "reference": "5617f01e40980723512f510cbacfd9a4671aaa5b",
+                "url": "https://api.github.com/repos/hyperf/engine/zipball/26e0b65fc2a63a00266e7124e221c6f3fb2c8e95",
+                "reference": "26e0b65fc2a63a00266e7124e221c6f3fb2c8e95",
                 "shasum": ""
             },
             "require": {
-                "hyperf/engine-contract": "~1.9.0",
+                "hyperf/engine-contract": "~1.10.0",
                 "php": ">=8.0"
             },
             "conflict": {
@@ -2398,7 +2398,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.10-dev"
+                    "dev-master": "2.11-dev"
                 },
                 "hyperf": {
                     "config": "Hyperf\\Engine\\ConfigProvider"
@@ -2425,7 +2425,7 @@
             ],
             "support": {
                 "issues": "https://github.com/hyperf/engine/issues",
-                "source": "https://github.com/hyperf/engine/tree/v2.10.6"
+                "source": "https://github.com/hyperf/engine/tree/v2.11.0"
             },
             "funding": [
                 {
@@ -2437,20 +2437,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-04-09T11:34:48+00:00"
+            "time": "2024-04-17T13:36:28+00:00"
         },
         {
             "name": "hyperf/engine-contract",
-            "version": "v1.9.1",
+            "version": "v1.10.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/engine-contract.git",
-                "reference": "fec2e45f35404b2e5b4c3eaf1b0dce67d60771eb"
+                "reference": "2714a8ba6d6b916e5bd373ff680df9569a4c9eef"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/engine-contract/zipball/fec2e45f35404b2e5b4c3eaf1b0dce67d60771eb",
-                "reference": "fec2e45f35404b2e5b4c3eaf1b0dce67d60771eb",
+                "url": "https://api.github.com/repos/hyperf/engine-contract/zipball/2714a8ba6d6b916e5bd373ff680df9569a4c9eef",
+                "reference": "2714a8ba6d6b916e5bd373ff680df9569a4c9eef",
                 "shasum": ""
             },
             "require": {
@@ -2492,7 +2492,7 @@
             ],
             "support": {
                 "issues": "https://github.com/hyperf/engine-contract/issues",
-                "source": "https://github.com/hyperf/engine-contract/tree/v1.9.1"
+                "source": "https://github.com/hyperf/engine-contract/tree/v1.10.1"
             },
             "funding": [
                 {
@@ -2504,7 +2504,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-12-15T07:37:14+00:00"
+            "time": "2024-04-17T13:34:51+00:00"
         },
         {
             "name": "hyperf/event",
@@ -2826,16 +2826,16 @@
         },
         {
             "name": "hyperf/stringable",
-            "version": "v3.1.15",
+            "version": "v3.1.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/stringable.git",
-                "reference": "6cbd6f220d833c3f2c28c8263dccffee48021dcb"
+                "reference": "4997554ca2450c485446704adb9ed7c18ea93c70"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/stringable/zipball/6cbd6f220d833c3f2c28c8263dccffee48021dcb",
-                "reference": "6cbd6f220d833c3f2c28c8263dccffee48021dcb",
+                "url": "https://api.github.com/repos/hyperf/stringable/zipball/4997554ca2450c485446704adb9ed7c18ea93c70",
+                "reference": "4997554ca2450c485446704adb9ed7c18ea93c70",
                 "shasum": ""
             },
             "require": {
@@ -2893,7 +2893,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-03-23T11:28:51+00:00"
+            "time": "2024-04-03T03:21:14+00:00"
         },
         {
             "name": "hyperf/support",
@@ -3151,16 +3151,16 @@
         },
         {
             "name": "lcobucci/jwt",
-            "version": "5.2.0",
+            "version": "5.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/lcobucci/jwt.git",
-                "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211"
+                "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0ba88aed12c04bd2ed9924f500673f32b67a6211",
-                "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211",
+                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
+                "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
                 "shasum": ""
             },
             "require": {
@@ -3208,7 +3208,7 @@
             ],
             "support": {
                 "issues": "https://github.com/lcobucci/jwt/issues",
-                "source": "https://github.com/lcobucci/jwt/tree/5.2.0"
+                "source": "https://github.com/lcobucci/jwt/tree/5.3.0"
             },
             "funding": [
                 {
@@ -3220,7 +3220,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2023-11-20T21:17:42+00:00"
+            "time": "2024-04-11T23:07:54+00:00"
         },
         {
             "name": "league/commonmark",
@@ -7806,22 +7806,22 @@
         },
         {
             "name": "twig/cache-extra",
-            "version": "v3.8.0",
+            "version": "v3.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/cache-extra.git",
-                "reference": "1e78b0e9268dd9742c480a2279eb246652a9c452"
+                "reference": "317ec85c3831ef08003364acfa291c160f393e5d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/cache-extra/zipball/1e78b0e9268dd9742c480a2279eb246652a9c452",
-                "reference": "1e78b0e9268dd9742c480a2279eb246652a9c452",
+                "url": "https://api.github.com/repos/twigphp/cache-extra/zipball/317ec85c3831ef08003364acfa291c160f393e5d",
+                "reference": "317ec85c3831ef08003364acfa291c160f393e5d",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.2.5",
-                "symfony/cache": "^5.0|^6.0|^7.0",
-                "twig/twig": "^3.0"
+                "symfony/cache": "^5.4|^6.4|^7.0",
+                "twig/twig": "^3.9"
             },
             "require-dev": {
                 "symfony/phpunit-bridge": "^6.4|^7.0"
@@ -7855,7 +7855,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/twigphp/cache-extra/tree/v3.8.0"
+                "source": "https://github.com/twigphp/cache-extra/tree/v3.9.0"
             },
             "funding": [
                 {
@@ -7867,34 +7867,41 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-11-21T14:02:01+00:00"
+            "time": "2024-02-10T08:52:03+00:00"
         },
         {
             "name": "twig/twig",
-            "version": "v3.8.0",
+            "version": "v3.9.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
+                "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
-                "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58",
+                "reference": "a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.2.5",
+                "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-ctype": "^1.8",
                 "symfony/polyfill-mbstring": "^1.3",
                 "symfony/polyfill-php80": "^1.22"
             },
             "require-dev": {
                 "psr/container": "^1.0|^2.0",
-                "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
+                "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
             },
             "type": "library",
             "autoload": {
+                "files": [
+                    "src/Resources/core.php",
+                    "src/Resources/debug.php",
+                    "src/Resources/escaper.php",
+                    "src/Resources/string_loader.php"
+                ],
                 "psr-4": {
                     "Twig\\": "src/"
                 }
@@ -7927,7 +7934,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.8.0"
+                "source": "https://github.com/twigphp/Twig/tree/v3.9.3"
             },
             "funding": [
                 {
@@ -7939,7 +7946,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-11-21T18:54:41+00:00"
+            "time": "2024-04-18T11:59:33+00:00"
         },
         {
             "name": "webmozart/assert",
@@ -8769,16 +8776,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "11.1.1",
+            "version": "11.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "816cb6070af901c0548aa5f18f2132fd8a6e4ade"
+                "reference": "51e342a0bc987e0ea8418105d0711f08ae116de3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/816cb6070af901c0548aa5f18f2132fd8a6e4ade",
-                "reference": "816cb6070af901c0548aa5f18f2132fd8a6e4ade",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/51e342a0bc987e0ea8418105d0711f08ae116de3",
+                "reference": "51e342a0bc987e0ea8418105d0711f08ae116de3",
                 "shasum": ""
             },
             "require": {
@@ -8849,7 +8856,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.1"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.2"
             },
             "funding": [
                 {
@@ -8865,7 +8872,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-06T06:20:21+00:00"
+            "time": "2024-04-14T07:13:56+00:00"
         },
         {
             "name": "sebastian/cli-parser",
diff --git a/docs/pages/docs/features/observability/observable-task-table/index.md b/docs/pages/docs/features/observability/observable-task-table/index.md
index 8ea9b77a24c4c1b7518cea72acba810952ee769e..79ffb3429181e4a38b5a5a6e1be33abd6dfcee0f 100644
--- a/docs/pages/docs/features/observability/observable-task-table/index.md
+++ b/docs/pages/docs/features/observability/observable-task-table/index.md
@@ -40,37 +40,6 @@ $this->observableTaskTable->observe(new ObservableTask(
 ));
 ```
 
-## Observing Task with Timeout
-
-The `ObservableTaskTimeoutIterator` class is used to define a timeout for the 
-task. If the task is inactive for the specified time, the task will be 
-cancelled.
-
-You can use it by composing it with an observable task:
-
-```php
-$observableTaskTimeout = new ObservableTaskTimeoutIterator(
-    iterableTask: function () use (
-        $webSocketAuthResolution,
-        $webSocketConnection,
-        $rpcRequest,
-    ): Generator {
-        yield new ObservableTaskStatusUpdate(ObservableTaskStatus::Running, null);
-
-        Coroutine::sleep(3);
-
-        yield new ObservableTaskStatusUpdate(ObservableTaskStatus::Finished, null);
-    },
-    inactivityTimeout: 5.0,
-);
-
-$this->observableTaskTable->observe(new ObservableTask(
-    iterableTask: $observableTaskTimeout,
-    name: 'test',
-    category: 'test_tasks',
-));
-```
-
 # Rendering
 
 Observable tasks table is iterable and uses shared memory. It is possible to
diff --git a/docs/pages/index.md b/docs/pages/index.md
index c6c73a6603c7ef7993d57742f438ce155de50bdb..560f6dc5a67ce76d7d3df735b5dae1042e092fcd 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -17,11 +17,13 @@ description: >
     <div class="homepage__content">
         <hgroup class="homepage__title">
             <h1>PHP Framework for Next-Gen Web Apps</h1>
-            <h2>Resonance: Connect, Converse, Create</h2>
+            <h2>
+                Connect, Converse, Create
+            </h2>
             <p>
-                PHP Resonance Framework is designed from the ground up to 
-                facilitate interoperability and messaging between services in 
-                your infrastructure and beyond. It provides AI capabilities,
+                Resonance is designed from the ground up to facilitate 
+                interoperability and messaging between services in your 
+                infrastructure and beyond. It provides AI capabilities,
                 has a built in web server and integrates with llama.cpp.
             </p>
             <p>
@@ -133,22 +135,6 @@ description: >
                 href="/docs/features/validation/constraints/"
             >Learn More</a>
         </li>
-        <li class="homepage-gallery__item">
-            <h4>
-                <a href="/docs/features/ai/prompt-subject-responders/">
-                    Prompt Subject Responders
-                    <span class="homepage-gallery__version">v0.20.0</span>
-                </a>
-            </h4>
-            <iframe 
-                src="https://www.youtube.com/embed/pCyEBueNw24" 
-                title="YouTube video player" 
-                frameborder="0" 
-                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
-                referrerpolicy="strict-origin-when-cross-origin" 
-                allowfullscreen
-            ></iframe>
-        </li>
         <li class="homepage-gallery__item">
             <h4>
                 <a href="/docs/features/swoole-server-tasks/">
diff --git a/resources/css/docs-page-homepage.css b/resources/css/docs-page-homepage.css
index dc815085cabda65265d2eccfc78c6753b0ad8d4c..483f41802348a0d6e8a6e9018a408146f560a964 100644
--- a/resources/css/docs-page-homepage.css
+++ b/resources/css/docs-page-homepage.css
@@ -200,6 +200,11 @@ h2.homepage__example__title {
   }
 }
 
+.homepage__title h3 {
+  font-size: 2em;
+  font-weight: bold;
+}
+
 .homepage__title p {
   line-height: 1.5;
   max-width: 80ch;
diff --git a/src/Command/StaticPagesBuild.php b/src/Command/StaticPagesBuild.php
index 41a2fc0fea5a5a183ada3725169f8f75c094db32..747e20e9a618f5c3511180979eb4e5e2e380b2bb 100644
--- a/src/Command/StaticPagesBuild.php
+++ b/src/Command/StaticPagesBuild.php
@@ -9,10 +9,11 @@ use Distantmagic\Resonance\Attribute\WantsFeature;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\StaticPageProcessor;
-use Distantmagic\Resonance\SwooleCoroutineHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 #[ConsoleCommand(
     name: 'static-pages:build',
     description: 'Generate static pages'
@@ -28,7 +29,7 @@ final class StaticPagesBuild extends Command
 
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        SwooleCoroutineHelper::mustRun(function () {
+        coroutineMustRun(function () {
             $this->staticPageProcessor->process();
         });
 
diff --git a/src/CoroutineCommand.php b/src/CoroutineCommand.php
index 193b34fa4de861c090b8f44ced4c87749737abc1..ddb4fb0e9fb25e646a724d3a4bc365b0cbe6f0eb 100644
--- a/src/CoroutineCommand.php
+++ b/src/CoroutineCommand.php
@@ -8,6 +8,8 @@ use Symfony\Component\Console\Command\Command as SymfonyCommand;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 abstract class CoroutineCommand extends SymfonyCommand
 {
     abstract protected function executeInCoroutine(InputInterface $input, OutputInterface $output): int;
@@ -29,7 +31,7 @@ abstract class CoroutineCommand extends SymfonyCommand
             'log_level' => $this->swooleConfiguration->logLevel,
         ]);
 
-        return SwooleCoroutineHelper::mustRun(function () use ($input, $output): int {
+        return coroutineMustRun(function () use ($input, $output): int {
             return $this->executeInCoroutine($input, $output);
         });
     }
diff --git a/src/CronJobRunner.php b/src/CronJobRunner.php
index ccca13403dccb9a6c46cee0f6cc1d20c618c37f2..c9e97e4f4576f946e837b25a81af060be524912d 100644
--- a/src/CronJobRunner.php
+++ b/src/CronJobRunner.php
@@ -7,6 +7,8 @@ namespace Distantmagic\Resonance;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Psr\Log\LoggerInterface;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGo;
+
 #[Singleton]
 readonly class CronJobRunner
 {
@@ -14,7 +16,7 @@ readonly class CronJobRunner
 
     public function runCronJob(CronRegisteredJob $cronRegisteredJob): void
     {
-        SwooleCoroutineHelper::mustGo(function () use ($cronRegisteredJob): void {
+        coroutineMustGo(function () use ($cronRegisteredJob): void {
             $this->logger->info(sprintf('cron_job_start(%s)', $cronRegisteredJob->name));
             $cronRegisteredJob->cronJob->onCronTick();
         });
diff --git a/src/DependencyInjectionContainer.php b/src/DependencyInjectionContainer.php
index 0f04e6e6d7896cf5e669d60d9f160b23b5877974..2bc80ef7b4ec56b1d324f9600ec5871f1d0680ff 100644
--- a/src/DependencyInjectionContainer.php
+++ b/src/DependencyInjectionContainer.php
@@ -18,6 +18,8 @@ use ReflectionFunction;
 use ReflectionFunctionAbstract;
 use Swoole\Event;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 readonly class DependencyInjectionContainer
 {
     public PHPProjectFiles $phpProjectFiles;
@@ -76,7 +78,7 @@ readonly class DependencyInjectionContainer
         /**
          * @var null|array<string,mixed>
          */
-        $parameters = SwooleCoroutineHelper::mustRun(function () use ($function): array {
+        $parameters = coroutineMustRun(function () use ($function): array {
             $reflectionFunction = new ReflectionFunction($function);
 
             return $this->makeParameters($reflectionFunction, new DependencyStack());
@@ -122,7 +124,7 @@ readonly class DependencyInjectionContainer
      */
     public function make(string $className): object
     {
-        $ret = SwooleCoroutineHelper::mustRun(function () use ($className): object {
+        $ret = coroutineMustRun(function () use ($className): object {
             $stack = new DependencyStack();
 
             if ($this->dependencyProviders->hasKey($className)) {
diff --git a/src/DialogueMessageProducer/LlamaCppPromptMessageProducer.php b/src/DialogueMessageProducer/LlamaCppPromptMessageProducer.php
index cc3c68db15727d43cbd8f285db4ba8af35032541..e659022da0f9c38a548a338f11f5c0caca1263a6 100644
--- a/src/DialogueMessageProducer/LlamaCppPromptMessageProducer.php
+++ b/src/DialogueMessageProducer/LlamaCppPromptMessageProducer.php
@@ -27,22 +27,10 @@ readonly class LlamaCppPromptMessageProducer extends DialogueMessageProducer
         );
 
         foreach ($completion as $token) {
-            if ($token->isFailed) {
-                $completion->stop();
-
-                yield new DialogueMessageChunk(
-                    content: '',
-                    isFailed: true,
-                    isLastToken: true,
-                );
-
-                return;
-            }
-
             yield new DialogueMessageChunk(
                 content: (string) $token,
-                isFailed: false,
-                isLastToken: $token->isLastToken,
+                isFailed: $token->isFailed(),
+                isLastToken: $token->isLastToken(),
             );
         }
     }
diff --git a/src/DialogueNode.php b/src/DialogueNode.php
index 83ac2456eaa4c5eaea09edcee38c0f560fd573ee..260575f20e0c1be072e51e82d820d8c5bc4c905c 100644
--- a/src/DialogueNode.php
+++ b/src/DialogueNode.php
@@ -6,6 +6,9 @@ namespace Distantmagic\Resonance;
 
 use Distantmagic\Resonance\DialogueMessageProducer\ConstMessageProducer;
 use Ds\Set;
+use Generator;
+
+use function Distantmagic\Resonance\helpers\generatorGetReturn;
 
 readonly class DialogueNode implements DialogueNodeInterface
 {
@@ -69,9 +72,20 @@ readonly class DialogueNode implements DialogueNodeInterface
     }
 
     public function respondTo(DialogueInputInterface $dialogueInput): DialogueResponseResolutionInterface
+    {
+        $respondToGenerator = $this->respondToWithProgress($dialogueInput);
+
+        return generatorGetReturn($respondToGenerator);
+    }
+
+    public function respondToWithProgress(DialogueInputInterface $dialogueInput): Generator
     {
         foreach (new DialogueResponseSortedIterator($this->responses) as $response) {
-            $resolution = $response->resolveResponse($dialogueInput);
+            $resolutionGenerator = $response->resolveResponseWithProgress($dialogueInput);
+
+            yield from $resolutionGenerator;
+
+            $resolution = $resolutionGenerator->getReturn();
             $resolutionStatus = $resolution->getStatus();
 
             if ($resolutionStatus->isFailed() || $resolutionStatus->canRespond()) {
diff --git a/src/DialogueNodeInterface.php b/src/DialogueNodeInterface.php
index b0c5c3a5e963004482817fbb9de61e356d6afe0c..685b1e6a91d996ed9ea247383339dac7df4b2b56 100644
--- a/src/DialogueNodeInterface.php
+++ b/src/DialogueNodeInterface.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Ds\Set;
+use Generator;
 
 interface DialogueNodeInterface
 {
@@ -27,4 +28,9 @@ interface DialogueNodeInterface
     public function getSideEffects(): Set;
 
     public function respondTo(DialogueInputInterface $dialogueInput): DialogueResponseResolutionInterface;
+
+    /**
+     * @return Generator<mixed,LlmCompletionProgressInterface,mixed,DialogueResponseResolutionInterface>
+     */
+    public function respondToWithProgress(DialogueInputInterface $dialogueInput): Generator;
 }
diff --git a/src/DialogueNodeTest.php b/src/DialogueNodeTest.php
index 7ecad6711439e02377cf66ac7607adfa2910e1d3..c873947b911f916794c4dad3b668ae746b9a0474 100644
--- a/src/DialogueNodeTest.php
+++ b/src/DialogueNodeTest.php
@@ -13,6 +13,8 @@ use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\TestCase;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 /**
  * @internal
  */
@@ -53,14 +55,14 @@ final class DialogueNodeTest extends TestCase
             followUp: $marketingNode,
         ));
 
-        SwooleCoroutineHelper::mustRun(static function () use ($marketingNode, $rootNode) {
+        coroutineMustRun(static function () use ($marketingNode, $rootNode) {
             $response = $rootNode->respondTo(new UserInput('yep'));
 
             self::assertFalse($response->getStatus()->isFailed());
             self::assertSame($marketingNode, $response->getFollowUp());
         });
 
-        SwooleCoroutineHelper::mustRun(static function () use ($rootNode) {
+        coroutineMustRun(static function () use ($rootNode) {
             $response = $rootNode->respondTo(new UserInput('I do not know who I am'));
 
             self::assertFalse($response->getStatus()->isFailed());
@@ -127,7 +129,7 @@ final class DialogueNodeTest extends TestCase
             followUp: $marketingNode,
         ));
 
-        SwooleCoroutineHelper::mustRun(static function () use ($rootNode) {
+        coroutineMustRun(static function () use ($rootNode) {
             $response = $rootNode->respondTo(new UserInput('i am a recruiter'));
 
             self::assertFalse($response->getStatus()->isFailed());
diff --git a/src/DialogueResponse.php b/src/DialogueResponse.php
index 9940317fa4d3483e1722e95222db5bfdcbea1fbd..f86b47e8cdd6a8a460a51a7eaab95828d82a4f92 100644
--- a/src/DialogueResponse.php
+++ b/src/DialogueResponse.php
@@ -4,4 +4,14 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-abstract readonly class DialogueResponse implements DialogueResponseInterface {}
+use function Distantmagic\Resonance\helpers\generatorGetReturn;
+
+abstract readonly class DialogueResponse implements DialogueResponseInterface
+{
+    public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolutionInterface
+    {
+        $resolveGenerator = $this->resolveResponseWithProgress($dialogueInput);
+
+        return generatorGetReturn($resolveGenerator);
+    }
+}
diff --git a/src/DialogueResponse/CatchAllResponse.php b/src/DialogueResponse/CatchAllResponse.php
index 5e7cc9de20eae20d54a94e98e208e8f2c9f84fea..084738db8ff8dfd72f19ada622547eaeae4df8f9 100644
--- a/src/DialogueResponse/CatchAllResponse.php
+++ b/src/DialogueResponse/CatchAllResponse.php
@@ -9,6 +9,8 @@ use Distantmagic\Resonance\DialogueNodeInterface;
 use Distantmagic\Resonance\DialogueResponse;
 use Distantmagic\Resonance\DialogueResponseResolution;
 use Distantmagic\Resonance\DialogueResponseResolutionStatus;
+use Distantmagic\Resonance\LlmCompletionProgress;
+use Generator;
 
 readonly class CatchAllResponse extends DialogueResponse
 {
@@ -19,8 +21,13 @@ readonly class CatchAllResponse extends DialogueResponse
         return 100;
     }
 
-    public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolution
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator
     {
+        yield new LlmCompletionProgress(
+            category: 'catch_all_response',
+            shouldNotify: false,
+        );
+
         return new DialogueResponseResolution(
             followUp: $this->followUp,
             status: DialogueResponseResolutionStatus::CanRespond,
diff --git a/src/DialogueResponse/LiteralInputResponse.php b/src/DialogueResponse/LiteralInputResponse.php
index 6d95df7d4da90d349ca9cf3b4f737b6f67ebba05..41a5b4e950d8522073e618b300d743de7f84b036 100644
--- a/src/DialogueResponse/LiteralInputResponse.php
+++ b/src/DialogueResponse/LiteralInputResponse.php
@@ -9,6 +9,8 @@ use Distantmagic\Resonance\DialogueNodeInterface;
 use Distantmagic\Resonance\DialogueResponse;
 use Distantmagic\Resonance\DialogueResponseResolution;
 use Distantmagic\Resonance\DialogueResponseResolutionStatus;
+use Distantmagic\Resonance\LlmCompletionProgress;
+use Generator;
 
 readonly class LiteralInputResponse extends DialogueResponse
 {
@@ -22,8 +24,13 @@ readonly class LiteralInputResponse extends DialogueResponse
         return 2;
     }
 
-    public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolution
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator
     {
+        yield new LlmCompletionProgress(
+            category: 'literal_input_response',
+            shouldNotify: false,
+        );
+
         if ($dialogueInput->getContent() === $this->when) {
             return new DialogueResponseResolution(
                 followUp: $this->followUp,
diff --git a/src/DialogueResponse/LlamaCppExtractSubjectResponse.php b/src/DialogueResponse/LlamaCppExtractSubjectResponse.php
index 47d9ea2e79ba5c01171ca85644cddafeee4ca3d3..e217c0e9d41cb47c1fc17b274ea3aa7cd74b0c5f 100644
--- a/src/DialogueResponse/LlamaCppExtractSubjectResponse.php
+++ b/src/DialogueResponse/LlamaCppExtractSubjectResponse.php
@@ -11,18 +11,23 @@ use Distantmagic\Resonance\DialogueResponseResolution;
 use Distantmagic\Resonance\DialogueResponseResolutionStatus;
 use Distantmagic\Resonance\LlamaCppExtractSubjectInterface;
 use Distantmagic\Resonance\LlamaCppExtractSubjectResult;
+use Distantmagic\Resonance\LlmCompletionProgressInterface;
 use Distantmagic\Resonance\LlmPersona\HelpfulAssistant;
 use Distantmagic\Resonance\LlmPersonaInterface;
+use Generator;
 
+/**
+ * @psalm-type TCallbackReturn = DialogueResponseResolution|Generator<mixed,LlmCompletionProgressInterface,mixed,DialogueResponseResolution>
+ */
 readonly class LlamaCppExtractSubjectResponse extends DialogueResponse
 {
     /**
-     * @var Closure(LlamaCppExtractSubjectResult):DialogueResponseResolution $whenProvided
+     * @var Closure(LlamaCppExtractSubjectResult):TCallbackReturn $whenProvided
      */
     private Closure $whenProvided;
 
     /**
-     * @param callable(LlamaCppExtractSubjectResult):DialogueResponseResolution $whenProvided
+     * @param callable(LlamaCppExtractSubjectResult):TCallbackReturn $whenProvided
      */
     public function __construct(
         private LlamaCppExtractSubjectInterface $llamaCppExtractSubject,
@@ -38,14 +43,18 @@ readonly class LlamaCppExtractSubjectResponse extends DialogueResponse
         return 50;
     }
 
-    public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolution
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator
     {
-        $extracted = $this->llamaCppExtractSubject->extract(
+        $extractedGenerator = $this->llamaCppExtractSubject->extractWithProgress(
             input: $dialogueInput->getContent(),
             persona: $this->persona,
             topic: $this->topic,
         );
 
+        yield from $extractedGenerator;
+
+        $extracted = $extractedGenerator->getReturn();
+
         if ($extracted->isFailed) {
             return new DialogueResponseResolution(
                 followUp: null,
@@ -60,6 +69,14 @@ readonly class LlamaCppExtractSubjectResponse extends DialogueResponse
             );
         }
 
-        return ($this->whenProvided)($extracted);
+        $ret = ($this->whenProvided)($extracted);
+
+        if ($ret instanceof Generator) {
+            yield from $ret;
+
+            return $ret->getReturn();
+        }
+
+        return $ret;
     }
 }
diff --git a/src/DialogueResponse/LlamaCppExtractWhenResponse.php b/src/DialogueResponse/LlamaCppExtractWhenResponse.php
index f3ad6d93d00d6b919e1d5ac91265eb14a088a469..67f98373f57c7d788218d2af84a429abe38286f0 100644
--- a/src/DialogueResponse/LlamaCppExtractWhenResponse.php
+++ b/src/DialogueResponse/LlamaCppExtractWhenResponse.php
@@ -11,18 +11,23 @@ use Distantmagic\Resonance\DialogueResponseResolution;
 use Distantmagic\Resonance\DialogueResponseResolutionStatus;
 use Distantmagic\Resonance\LlamaCppExtractWhenInterface;
 use Distantmagic\Resonance\LlamaCppExtractWhenResult;
+use Distantmagic\Resonance\LlmCompletionProgressInterface;
 use Distantmagic\Resonance\LlmPersona\HelpfulAssistant;
 use Distantmagic\Resonance\LlmPersonaInterface;
+use Generator;
 
+/**
+ * @psalm-type TCallbackReturn = DialogueResponseResolution|Generator<mixed,LlmCompletionProgressInterface,mixed,DialogueResponseResolution>
+ */
 readonly class LlamaCppExtractWhenResponse extends DialogueResponse
 {
     /**
-     * @var Closure(LlamaCppExtractWhenResult):DialogueResponseResolution $whenProvided
+     * @var Closure(LlamaCppExtractWhenResult):TCallbackReturn $whenProvided
      */
     private Closure $whenProvided;
 
     /**
-     * @param callable(LlamaCppExtractWhenResult):DialogueResponseResolution $whenProvided
+     * @param callable(LlamaCppExtractWhenResult):TCallbackReturn $whenProvided
      */
     public function __construct(
         private LlamaCppExtractWhenInterface $llamaCppExtractWhen,
@@ -38,15 +43,18 @@ readonly class LlamaCppExtractWhenResponse extends DialogueResponse
         return 50;
     }
 
-    public function resolveResponse(
-        DialogueInputInterface $dialogueInput
-    ): DialogueResponseResolution {
-        $extracted = $this->llamaCppExtractWhen->extract(
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator
+    {
+        $extractedGenerator = $this->llamaCppExtractWhen->extractWithProgress(
             condition: $this->condition,
             input: $dialogueInput->getContent(),
             persona: $this->persona,
         );
 
+        yield from $extractedGenerator;
+
+        $extracted = $extractedGenerator->getReturn();
+
         if ($extracted->isFailed) {
             return new DialogueResponseResolution(
                 followUp: null,
@@ -61,6 +69,14 @@ readonly class LlamaCppExtractWhenResponse extends DialogueResponse
             );
         }
 
-        return ($this->whenProvided)($extracted);
+        $ret = ($this->whenProvided)($extracted);
+
+        if ($ret instanceof Generator) {
+            yield from $ret;
+
+            return $ret->getReturn();
+        }
+
+        return $ret;
     }
 }
diff --git a/src/DialogueResponse/LlamaCppExtractYesNoMaybeResponse.php b/src/DialogueResponse/LlamaCppExtractYesNoMaybeResponse.php
index b631ee041b2f4060480cd3d235ff4b421f130603..a22f8ec0c797ca69c87a5d84e6ab1b9791d5a130 100644
--- a/src/DialogueResponse/LlamaCppExtractYesNoMaybeResponse.php
+++ b/src/DialogueResponse/LlamaCppExtractYesNoMaybeResponse.php
@@ -11,16 +11,22 @@ use Distantmagic\Resonance\DialogueResponseResolution;
 use Distantmagic\Resonance\DialogueResponseResolutionStatus;
 use Distantmagic\Resonance\LlamaCppExtractYesNoMaybe;
 use Distantmagic\Resonance\LlamaCppExtractYesNoMaybeResult;
+use Distantmagic\Resonance\LlmCompletionProgress;
+use Distantmagic\Resonance\LlmCompletionProgressInterface;
+use Generator;
 
+/**
+ * @psalm-type TCallbackReturn = DialogueResponseResolution|Generator<mixed,LlmCompletionProgressInterface,mixed,DialogueResponseResolution>
+ */
 readonly class LlamaCppExtractYesNoMaybeResponse extends DialogueResponse
 {
     /**
-     * @var Closure(LlamaCppExtractYesNoMaybeResult):DialogueResponseResolution $whenProvided
+     * @var Closure(LlamaCppExtractYesNoMaybeResult):TCallbackReturn $whenProvided
      */
     private Closure $whenProvided;
 
     /**
-     * @param callable(LlamaCppExtractYesNoMaybeResult):DialogueResponseResolution $whenProvided
+     * @param callable(LlamaCppExtractYesNoMaybeResult):TCallbackReturn $whenProvided
      */
     public function __construct(
         private LlamaCppExtractYesNoMaybe $llamaCppExtractYesNoMaybe,
@@ -34,10 +40,15 @@ readonly class LlamaCppExtractYesNoMaybeResponse extends DialogueResponse
         return 50;
     }
 
-    public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolution
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator
     {
         $extracted = $this->llamaCppExtractYesNoMaybe->extract(input: $dialogueInput->getContent());
 
+        yield new LlmCompletionProgress(
+            category: 'extract_yes_no_maybe_response',
+            shouldNotify: false,
+        );
+
         if (is_null($extracted->result) || $extracted->isFailed) {
             return new DialogueResponseResolution(
                 followUp: null,
@@ -45,6 +56,14 @@ readonly class LlamaCppExtractYesNoMaybeResponse extends DialogueResponse
             );
         }
 
-        return ($this->whenProvided)($extracted);
+        $ret = ($this->whenProvided)($extracted);
+
+        if ($ret instanceof Generator) {
+            yield from $ret;
+
+            return $ret->getReturn();
+        }
+
+        return $ret;
     }
 }
diff --git a/src/DialogueResponseInterface.php b/src/DialogueResponseInterface.php
index 8eba99612df548cefd5fa2da8b23bbc534697682..5e79a92e7009fbd1f1b4a8820270b657f2ae6722 100644
--- a/src/DialogueResponseInterface.php
+++ b/src/DialogueResponseInterface.php
@@ -4,9 +4,16 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use Generator;
+
 interface DialogueResponseInterface
 {
     public function getCost(): int;
 
     public function resolveResponse(DialogueInputInterface $dialogueInput): DialogueResponseResolutionInterface;
+
+    /**
+     * @return Generator<mixed,LlmCompletionProgressInterface,mixed,DialogueResponseResolutionInterface>
+     */
+    public function resolveResponseWithProgress(DialogueInputInterface $dialogueInput): Generator;
 }
diff --git a/src/DoctrineConsoleRunner.php b/src/DoctrineConsoleRunner.php
index 30ebc273d00471de2034f3e78036d8cbfd018359..d50be6405101dbf6f53785f72e99f61ceb7fb647 100644
--- a/src/DoctrineConsoleRunner.php
+++ b/src/DoctrineConsoleRunner.php
@@ -8,6 +8,8 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
 use Swoole\Runtime;
 use Symfony\Component\Console\Application;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 final readonly class DoctrineConsoleRunner
 {
     public static function run(DependencyInjectionContainer $container): never
@@ -22,7 +24,7 @@ final readonly class DoctrineConsoleRunner
 
             ConsoleRunner::addCommands($cli, $entityManagerProvider);
 
-            $errorCode = SwooleCoroutineHelper::mustRun(static function () use ($cli): int {
+            $errorCode = coroutineMustRun(static function () use ($cli): int {
                 return $cli->run();
             });
 
diff --git a/src/GeneratorHelper.php b/src/GeneratorHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..24e37650032e24dc31be7ee4560e471ffedf3a25
--- /dev/null
+++ b/src/GeneratorHelper.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+final readonly class GeneratorHelper {}
diff --git a/src/HttpResponderAggregate.php b/src/HttpResponderAggregate.php
index 0ee1b574783e46818f6b8b9f8f28289e31b5169d..c69153eb2bff9b10f9bd269cfbd5fb7a5fe22a37 100644
--- a/src/HttpResponderAggregate.php
+++ b/src/HttpResponderAggregate.php
@@ -27,6 +27,8 @@ use Symfony\Component\Routing\Matcher\UrlMatcher;
 use Symfony\Component\Routing\RequestContext;
 use Throwable;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGetContext;
+
 #[Singleton]
 readonly class HttpResponderAggregate implements RequestHandlerInterface
 {
@@ -64,7 +66,7 @@ readonly class HttpResponderAggregate implements RequestHandlerInterface
         $responder = $this->selectResponder($request);
 
         try {
-            $context = SwooleCoroutineHelper::mustGetContext();
+            $context = coroutineMustGetContext();
 
             $context[SwooleContextRequestResponseReader::CONTEXT_KEY_REQUEST] = $request;
             $context[SwooleContextRequestResponseReader::CONTEXT_KEY_RESPONSE] = $response;
diff --git a/src/LlamaCppClient.php b/src/LlamaCppClient.php
index c12be7f6e3ecf6afe15ee9df55b8b48ea08859c6..6e9229d946e019708a9cbbcd32236c88580fbe66 100644
--- a/src/LlamaCppClient.php
+++ b/src/LlamaCppClient.php
@@ -13,6 +13,8 @@ use RuntimeException;
 use Swoole\Coroutine;
 use Swoole\Coroutine\Channel;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGo;
+
 #[RequiresPhpExtension('curl')]
 #[Singleton(provides: LlamaCppClientInterface::class)]
 readonly class LlamaCppClient implements LlamaCppClientInterface
@@ -172,7 +174,7 @@ readonly class LlamaCppClient implements LlamaCppClientInterface
     {
         $channel = new Channel(1);
 
-        SwooleCoroutineHelper::mustGo(function () use ($channel, $path, $requestData): void {
+        coroutineMustGo(function () use ($channel, $path, $requestData): void {
             $curlHandle = $this->createCurlHandle();
 
             Coroutine::defer(static function () use ($channel) {
diff --git a/src/LlamaCppClientTest.php b/src/LlamaCppClientTest.php
index 5b5f697f08e25a346822b996bc9975418d124df5..c1abf98c682fa40aff1ccdc2e8b97f73b90ced4c 100644
--- a/src/LlamaCppClientTest.php
+++ b/src/LlamaCppClientTest.php
@@ -9,6 +9,8 @@ use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\TestCase;
 use Swoole\Event;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 /**
  * @internal
  */
@@ -27,7 +29,7 @@ final class LlamaCppClientTest extends TestCase
     {
         $llamaCppClient = self::$container->make(LlamaCppClient::class);
 
-        SwooleCoroutineHelper::mustRun(static function () use ($llamaCppClient) {
+        coroutineMustRun(static function () use ($llamaCppClient) {
             $completion = $llamaCppClient->generateCompletion(new LlamaCppCompletionRequest(
                 llmChatHistory: new LlmChatHistory([
                     new LlmChatMessage('user', 'Who are you? Answer in exactly two words.'),
diff --git a/src/LlamaCppCompletionIterator.php b/src/LlamaCppCompletionIterator.php
index f4eb21a75721b09b902f67a716a8fb4e46f41fa8..7c40d38840d7bb67fb77e1c34041b5b162170342 100644
--- a/src/LlamaCppCompletionIterator.php
+++ b/src/LlamaCppCompletionIterator.php
@@ -34,42 +34,46 @@ readonly class LlamaCppCompletionIterator implements IteratorAggregate
          */
         $previousChunk = '';
 
-        foreach ($this->responseChunks as $responseChunk) {
-            if ($responseChunk instanceof SwooleChannelIteratorError || ObservableTaskStatus::Failed === $responseChunk->data->status) {
-                yield new LlamaCppCompletionToken(
-                    content: '',
-                    isFailed: true,
-                    isLastToken: true,
-                );
+        try {
+            foreach ($this->responseChunks as $responseChunk) {
+                if ($responseChunk instanceof SwooleChannelIteratorError || ObservableTaskStatus::Failed === $responseChunk->data->status) {
+                    yield new LlamaCppCompletionToken(
+                        content: '',
+                        isFailed: true,
+                        isLastToken: true,
+                    );
 
-                break;
-            }
+                    break;
+                }
 
-            $previousChunk .= $responseChunk->data->chunk;
+                $previousChunk .= $responseChunk->data->chunk;
 
-            /**
-             * @var null|object{
-             *   content: string,
-             *   stop: boolean,
-             * }
-             */
-            $unserializedToken = $this->jsonSerializer->unserialize(
-                json: $previousChunk,
-                offset: self::COMPLETION_CHUNKED_DATA_PREFIX_LENGTH,
-                throw: false,
-            );
+                /**
+                 * @var null|object{
+                 *   content: string,
+                 *   stop: boolean,
+                 * }
+                 */
+                $unserializedToken = $this->jsonSerializer->unserialize(
+                    json: $previousChunk,
+                    offset: self::COMPLETION_CHUNKED_DATA_PREFIX_LENGTH,
+                    throw: false,
+                );
 
-            if (JSON_ERROR_NONE === json_last_error()) {
-                $previousChunk = '';
-            }
+                if (JSON_ERROR_NONE === json_last_error()) {
+                    $previousChunk = '';
+                }
 
-            if ($unserializedToken) {
-                yield new LlamaCppCompletionToken(
-                    content: $unserializedToken->content,
-                    isFailed: false,
-                    isLastToken: $unserializedToken->stop,
-                );
+                if ($unserializedToken) {
+                    yield new LlamaCppCompletionToken(
+                        content: $unserializedToken->content,
+                        isFailed: false,
+                        isLastToken: $unserializedToken->stop,
+                    );
+                }
             }
+        } finally {
+            $this->stop();
         }
 
         if (!empty($previousChunk)) {
diff --git a/src/LlamaCppCompletionToken.php b/src/LlamaCppCompletionToken.php
index d475879cfdd3823b151dc4823abb79b70fc54588..d6a2d2657cfe9a74f34c6e1eab6622820403b6a5 100644
--- a/src/LlamaCppCompletionToken.php
+++ b/src/LlamaCppCompletionToken.php
@@ -9,16 +9,31 @@ use Stringable;
 /**
  * @psalm-suppress PossiblyUnusedProperty used in applications
  */
-readonly class LlamaCppCompletionToken implements Stringable
+readonly class LlamaCppCompletionToken implements LlmCompletionTokenInterface, Stringable
 {
     public function __construct(
-        public string $content,
-        public bool $isFailed,
-        public bool $isLastToken,
+        private string $content,
+        private bool $isFailed,
+        private bool $isLastToken,
     ) {}
 
     public function __toString(): string
     {
         return $this->content;
     }
+
+    public function getContent(): string
+    {
+        return $this->content;
+    }
+
+    public function isFailed(): bool
+    {
+        return $this->isFailed;
+    }
+
+    public function isLastToken(): bool
+    {
+        return $this->isLastToken;
+    }
 }
diff --git a/src/LlamaCppExtractSubject.php b/src/LlamaCppExtractSubject.php
index d18f8281362048057867ae87c072f90751981caf..32c691601e8670937eebac5a13ca4bd89d72cb11 100644
--- a/src/LlamaCppExtractSubject.php
+++ b/src/LlamaCppExtractSubject.php
@@ -7,6 +7,9 @@ namespace Distantmagic\Resonance;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\BackusNaurFormGrammar\InlineGrammar;
 use Distantmagic\Resonance\LlmPersona\HelpfulAssistant;
+use Generator;
+
+use function Distantmagic\Resonance\helpers\generatorGetReturn;
 
 #[Singleton(provides: LlamaCppExtractSubjectInterface::class)]
 readonly class LlamaCppExtractSubject implements LlamaCppExtractSubjectInterface
@@ -20,6 +23,16 @@ readonly class LlamaCppExtractSubject implements LlamaCppExtractSubjectInterface
         string $topic,
         LlmPersonaInterface $persona = new HelpfulAssistant(),
     ): LlamaCppExtractSubjectResult {
+        $extractGenerator = $this->extractWithProgress($input, $topic, $persona);
+
+        return generatorGetReturn($extractGenerator);
+    }
+
+    public function extractWithProgress(
+        string $input,
+        string $topic,
+        LlmPersonaInterface $persona = new HelpfulAssistant(),
+    ): Generator {
         $completion = $this->llamaCppClient->generateCompletion(
             new LlamaCppCompletionRequest(
                 backusNaurFormGrammar: new InlineGrammar('root ::= [0-9a-zA-Z\"\\\\\' ]+'),
@@ -46,9 +59,12 @@ readonly class LlamaCppExtractSubject implements LlamaCppExtractSubjectInterface
         $ret = '';
 
         foreach ($completion as $token) {
-            if ($token->isFailed) {
-                $completion->stop();
+            yield new LlmCompletionProgress(
+                category: 'extract_subject',
+                shouldNotify: true,
+            );
 
+            if ($token->isFailed()) {
                 return new LlamaCppExtractSubjectResult(
                     content: '',
                     input: $input,
@@ -93,5 +109,6 @@ readonly class LlamaCppExtractSubject implements LlamaCppExtractSubjectInterface
             isMatched: true,
             topic: $topic,
         );
+
     }
 }
diff --git a/src/LlamaCppExtractSubjectInterface.php b/src/LlamaCppExtractSubjectInterface.php
index 7a51bfdc9a0c5dc2d28197dafdadb9086d40c792..456635f4d44c276fc6b2a81e26fd0120f6e89403 100644
--- a/src/LlamaCppExtractSubjectInterface.php
+++ b/src/LlamaCppExtractSubjectInterface.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Distantmagic\Resonance\LlmPersona\HelpfulAssistant;
+use Generator;
 
 interface LlamaCppExtractSubjectInterface
 {
@@ -13,4 +14,13 @@ interface LlamaCppExtractSubjectInterface
         string $topic,
         LlmPersonaInterface $persona = new HelpfulAssistant(),
     ): LlamaCppExtractSubjectResult;
+
+    /**
+     * @return Generator<mixed,LlmCompletionProgressInterface,mixed,LlamaCppExtractSubjectResult>
+     */
+    public function extractWithProgress(
+        string $input,
+        string $topic,
+        LlmPersonaInterface $persona = new HelpfulAssistant(),
+    ): Generator;
 }
diff --git a/src/LlamaCppExtractSubjectTest.php b/src/LlamaCppExtractSubjectTest.php
index 89ae16b3c245e7cb293e8c46610a117b79078704..8470e5a80e8eb92ccbc2caceb131f04337f4221b 100644
--- a/src/LlamaCppExtractSubjectTest.php
+++ b/src/LlamaCppExtractSubjectTest.php
@@ -11,6 +11,8 @@ use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\TestCase;
 use Swoole\Event;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 /**
  * @internal
  */
@@ -75,7 +77,7 @@ final class LlamaCppExtractSubjectTest extends TestCase
     {
         $llamaCppExtract = self::$container->make(LlamaCppExtractSubject::class);
 
-        SwooleCoroutineHelper::mustRun(static function () use ($expected, $input, $llamaCppExtract, $topic) {
+        coroutineMustRun(static function () use ($expected, $input, $llamaCppExtract, $topic) {
             $extracted = $llamaCppExtract->extract(
                 topic: $topic,
                 input: $input,
diff --git a/src/LlamaCppExtractWhen.php b/src/LlamaCppExtractWhen.php
index 199343aa17346a829236835ec0faf800c4a0183f..96878586913565a10b52afa040f43d00796220a7 100644
--- a/src/LlamaCppExtractWhen.php
+++ b/src/LlamaCppExtractWhen.php
@@ -7,6 +7,9 @@ namespace Distantmagic\Resonance;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\BackusNaurFormGrammar\YesNoMaybeGrammar;
 use Distantmagic\Resonance\LlmPersona\HelpfulAssistant;
+use Generator;
+
+use function Distantmagic\Resonance\helpers\generatorGetReturn;
 
 #[Singleton(provides: LlamaCppExtractWhenInterface::class)]
 readonly class LlamaCppExtractWhen implements LlamaCppExtractWhenInterface
@@ -21,6 +24,16 @@ readonly class LlamaCppExtractWhen implements LlamaCppExtractWhenInterface
         string $condition,
         LlmPersonaInterface $persona = new HelpfulAssistant(),
     ): LlamaCppExtractWhenResult {
+        $extractGenerator = $this->extractWithProgress($input, $condition, $persona);
+
+        return generatorGetReturn($extractGenerator);
+    }
+
+    public function extractWithProgress(
+        string $input,
+        string $condition,
+        LlmPersonaInterface $persona = new HelpfulAssistant(),
+    ): Generator {
         $completion = $this->llamaCppClient->generateCompletion(
             new LlamaCppCompletionRequest(
                 backusNaurFormGrammar: $this->yesNoMaybeGrammar,
@@ -46,7 +59,12 @@ readonly class LlamaCppExtractWhen implements LlamaCppExtractWhenInterface
         $ret = '';
 
         foreach ($completion as $token) {
-            if ($token->isFailed) {
+            yield new LlmCompletionProgress(
+                category: 'extract_when',
+                shouldNotify: true,
+            );
+
+            if ($token->isFailed()) {
                 return new LlamaCppExtractWhenResult(
                     condition: $condition,
                     isFailed: true,
diff --git a/src/LlamaCppExtractWhenInterface.php b/src/LlamaCppExtractWhenInterface.php
index 99a1470647d230c5b17282174c834c2ea6432205..ae1cdb2992e0e6802f8f17a50325529c7c4687ea 100644
--- a/src/LlamaCppExtractWhenInterface.php
+++ b/src/LlamaCppExtractWhenInterface.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use Generator;
+
 interface LlamaCppExtractWhenInterface
 {
     public function extract(
@@ -11,4 +13,13 @@ interface LlamaCppExtractWhenInterface
         string $condition,
         LlmPersonaInterface $persona,
     ): LlamaCppExtractWhenResult;
+
+    /**
+     * @return Generator<mixed,LlmCompletionProgressInterface,mixed,LlamaCppExtractWhenResult>
+     */
+    public function extractWithProgress(
+        string $input,
+        string $condition,
+        LlmPersonaInterface $persona,
+    ): Generator;
 }
diff --git a/src/LlamaCppExtractWhenTest.php b/src/LlamaCppExtractWhenTest.php
index 229a3773a1383af871a1ad58bb686e684c38c39f..c17cee192b6c3b4b5d8458a58842f27a9a4d1dbc 100644
--- a/src/LlamaCppExtractWhenTest.php
+++ b/src/LlamaCppExtractWhenTest.php
@@ -11,6 +11,8 @@ use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\TestCase;
 use Swoole\Event;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 /**
  * @internal
  */
@@ -102,7 +104,7 @@ final class LlamaCppExtractWhenTest extends TestCase
     ): void {
         $llamaCppExtract = self::$container->make(LlamaCppExtractWhen::class);
 
-        SwooleCoroutineHelper::mustRun(static function () use ($expected, $input, $condition, $llamaCppExtract) {
+        coroutineMustRun(static function () use ($expected, $input, $condition, $llamaCppExtract) {
             $extracted = $llamaCppExtract->extract(
                 input: $input,
                 condition: $condition,
diff --git a/src/LlamaCppExtractYesNoMaybeTest.php b/src/LlamaCppExtractYesNoMaybeTest.php
index 90ea4a5e61fbbc66d54f98b9260b73b81b636fd6..96f6b2f38f435bade2fd8b96ada993f87e17b508 100644
--- a/src/LlamaCppExtractYesNoMaybeTest.php
+++ b/src/LlamaCppExtractYesNoMaybeTest.php
@@ -11,6 +11,8 @@ use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\TestCase;
 use Swoole\Event;
 
+use function Distantmagic\Resonance\helpers\coroutineMustRun;
+
 /**
  * @internal
  */
@@ -63,7 +65,7 @@ final class LlamaCppExtractYesNoMaybeTest extends TestCase
     {
         $llamaCppExtract = self::$container->make(LlamaCppExtractYesNoMaybe::class);
 
-        SwooleCoroutineHelper::mustRun(static function () use ($expected, $input, $llamaCppExtract) {
+        coroutineMustRun(static function () use ($expected, $input, $llamaCppExtract) {
             $extracted = $llamaCppExtract->extract(input: $input);
 
             self::assertSame($expected, $extracted->result);
diff --git a/src/LlmCompletionProgress.php b/src/LlmCompletionProgress.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d0d4d0a08441aaa9f8e32e4432ecbed8db15853
--- /dev/null
+++ b/src/LlmCompletionProgress.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+readonly class LlmCompletionProgress implements LlmCompletionProgressInterface
+{
+    public function __construct(
+        private string $category,
+        private bool $shouldNotify,
+    ) {}
+
+    public function getCategory(): string
+    {
+        return $this->category;
+    }
+
+    public function shouldNotify(): bool
+    {
+        return $this->shouldNotify;
+    }
+}
diff --git a/src/LlmCompletionProgressInterface.php b/src/LlmCompletionProgressInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..e8c535aa64e124f26e5e44f7cdc3e9fd01705e84
--- /dev/null
+++ b/src/LlmCompletionProgressInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface LlmCompletionProgressInterface
+{
+    public function getCategory(): string;
+
+    public function shouldNotify(): bool;
+}
diff --git a/src/LlmCompletionTokenInterface.php b/src/LlmCompletionTokenInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb1436a925baa00cd76a7975e7454b32b073e1fc
--- /dev/null
+++ b/src/LlmCompletionTokenInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface LlmCompletionTokenInterface
+{
+    public function getContent(): string;
+
+    public function isFailed(): bool;
+
+    public function isLastToken(): bool;
+}
diff --git a/src/ObservableTaskFactory.php b/src/ObservableTaskFactory.php
deleted file mode 100644
index 3bf9a5d155a26bed5906043b0b5eddb7360e3a5e..0000000000000000000000000000000000000000
--- a/src/ObservableTaskFactory.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Distantmagic\Resonance;
-
-use Generator;
-
-/**
- * @psalm-import-type TIterableTaskCallback from ObservableTask
- */
-final readonly class ObservableTaskFactory
-{
-    public static function withTimeout(
-        callable $iterableTask,
-        float $inactivityTimeout = 5.0,
-        string $name = '',
-        string $category = '',
-    ): ObservableTask {
-        return new ObservableTask(
-            iterableTask: new ObservableTaskTimeoutIterator(
-                iterableTask: static function () use ($iterableTask): Generator {
-                    yield new ObservableTaskStatusUpdate(ObservableTaskStatus::Running, null);
-
-                    yield from $iterableTask();
-                    yield new ObservableTaskStatusUpdate(ObservableTaskStatus::Finished, null);
-                },
-                inactivityTimeout: $inactivityTimeout,
-            ),
-            name: $name,
-            category: $category,
-        );
-    }
-
-    /**
-     * @psalm-suppress UnusedConstructor this class is just a wrapper around
-     *     functions
-     */
-    private function __construct() {}
-}
diff --git a/src/ObservableTaskTable.php b/src/ObservableTaskTable.php
index 5319b5a3bd5fba885874def772bd3e97c920c653..8b18725de84f8ca35d2ff9084baf8fd9094a2c06 100644
--- a/src/ObservableTaskTable.php
+++ b/src/ObservableTaskTable.php
@@ -12,6 +12,8 @@ use RuntimeException;
 use Swoole\Coroutine;
 use Swoole\Table;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGo;
+
 /**
  * @template-implements IteratorAggregate<non-empty-string,ObservableTaskTableRow>
  */
@@ -78,7 +80,7 @@ readonly class ObservableTaskTable implements IteratorAggregate
     {
         $slotId = $this->availableRowsPool->nextAvailableRow();
 
-        SwooleCoroutineHelper::mustGo(function () use ($slotId, $observableTask) {
+        coroutineMustGo(function () use ($slotId, $observableTask) {
             Coroutine::defer(function () use ($slotId) {
                 $this->availableRowsPool->freeAvailableRow($slotId);
             });
diff --git a/src/ObservableTaskTimeoutIterator.php b/src/ObservableTaskTimeoutIterator.php
deleted file mode 100644
index c815c0e03491c812fd561ece61c1873de617262f..0000000000000000000000000000000000000000
--- a/src/ObservableTaskTimeoutIterator.php
+++ /dev/null
@@ -1,121 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Distantmagic\Resonance;
-
-use Closure;
-use Generator;
-use IteratorAggregate;
-use Swoole\Coroutine;
-use Swoole\Coroutine\Channel;
-
-/**
- * @psalm-import-type TIterableTaskCallback from ObservableTask
- *
- * @template-implements IteratorAggregate<ObservableTaskStatusUpdate>
- */
-readonly class ObservableTaskTimeoutIterator implements IteratorAggregate
-{
-    /**
-     * @var Closure():iterable<ObservableTaskStatusUpdate>
-     */
-    private Closure $iterableTask;
-
-    /**
-     * @param TIterableTaskCallback $iterableTask
-     */
-    public function __construct(
-        callable $iterableTask,
-        private float $inactivityTimeout,
-    ) {
-        $this->iterableTask = Closure::fromCallable($iterableTask);
-    }
-
-    /**
-     * @return Generator<ObservableTaskStatusUpdate>
-     */
-    public function __invoke(): Generator
-    {
-        return $this->getIterator();
-    }
-
-    /**
-     * @psalm-suppress UnusedVariable $generatorCoroutineId is used, just asynchronously
-     *
-     * @return Generator<ObservableTaskStatusUpdate>
-     */
-    public function getIterator(): Generator
-    {
-        /**
-         * @var null|int $generatorCoroutineId
-         */
-        $generatorCoroutineId = null;
-
-        $channel = new Channel(1);
-
-        $swooleTimeout = new SwooleTimeout(static function () use ($channel, &$generatorCoroutineId) {
-            $channel->push(new ObservableTaskStatusUpdate(
-                ObservableTaskStatus::TimedOut,
-                null,
-            ));
-
-            if (is_int($generatorCoroutineId)) {
-                Coroutine::cancel($generatorCoroutineId);
-            }
-        });
-
-        $generatorCoroutineId = SwooleCoroutineHelper::mustGo(function () use ($channel, $swooleTimeout) {
-            $swooleTimeoutScheduled = $swooleTimeout->setTimeout($this->inactivityTimeout);
-
-            Coroutine::defer(static function () use ($channel) {
-                $channel->close();
-            });
-
-            Coroutine::defer(static function () use (&$swooleTimeoutScheduled) {
-                /**
-                 * @psalm-suppress UnnecessaryVarAnnotation it can be changed async
-                 *
-                 * @var null|SwooleTimeoutScheduled $swooleTimeoutScheduled
-                 */
-                $swooleTimeoutScheduled?->cancel();
-            });
-
-            foreach (($this->iterableTask)() as $observableTaskStatusUpdate) {
-                if (Coroutine::isCanceled()) {
-                    break;
-                }
-
-                $swooleTimeoutScheduled = $swooleTimeoutScheduled?->reschedule($this->inactivityTimeout);
-
-                if (!$swooleTimeoutScheduled) {
-                    break;
-                }
-
-                $channel->push($observableTaskStatusUpdate, $this->inactivityTimeout);
-            }
-        });
-
-        /**
-         * @var SwooleChannelIterator<ObservableTaskStatusUpdate>
-         */
-        $swooleChannelIterator = new SwooleChannelIterator(
-            channel: $channel,
-            timeout: $this->inactivityTimeout,
-        );
-
-        foreach ($swooleChannelIterator as $observableTaskStatusUpdate) {
-            if ($observableTaskStatusUpdate instanceof SwooleChannelIteratorError) {
-                if ($observableTaskStatusUpdate->isTimeout) {
-                    yield new ObservableTaskStatusUpdate(ObservableTaskStatus::TimedOut, null);
-                } else {
-                    yield new ObservableTaskStatusUpdate(ObservableTaskStatus::Failed, null);
-                }
-
-                break;
-            }
-
-            yield $observableTaskStatusUpdate->data;
-        }
-    }
-}
diff --git a/src/PHPFileReflectionClassIterator.php b/src/PHPFileReflectionClassIterator.php
index bbbad205efeabe60426a9d1b86f3955d85af94b9..b752aba62a42ca94d0c6f93e57f95498535d4304 100644
--- a/src/PHPFileReflectionClassIterator.php
+++ b/src/PHPFileReflectionClassIterator.php
@@ -66,10 +66,6 @@ readonly class PHPFileReflectionClassIterator implements IteratorAggregate
             return null;
         }
 
-        if (str_starts_with($file->getPath(), DM_RESONANCE_ROOT)) {
-            return $this->readKnownClassName($file, 'Distantmagic\\Resonance\\');
-        }
-
         $namespace = (new PHPFileNamespaceReader($file))->readNamespace();
 
         if (is_null($namespace)) {
@@ -94,24 +90,4 @@ readonly class PHPFileReflectionClassIterator implements IteratorAggregate
 
         return null;
     }
-
-    /**
-     * This is an optimization to not tokenize all the files.
-     *
-     * @return class-string
-     */
-    private function readKnownClassName(SplFileInfo $file, string $namespace): string
-    {
-        $relativeFilename = str_replace(
-            DIRECTORY_SEPARATOR,
-            '\\',
-            substr(
-                trim(substr($file->getPathname(), strlen(DM_RESONANCE_ROOT)), DIRECTORY_SEPARATOR),
-                0,
-                -4,
-            )
-        );
-
-        return $this->assertClassExists($namespace.$relativeFilename);
-    }
 }
diff --git a/src/PHPFileReflectionFunctionIterator.php b/src/PHPFileReflectionFunctionIterator.php
index 0dcc6f0706b855421595b960ca08059608d21356..fca7265930dbee57c5751f8a6b6649021ca38b54 100644
--- a/src/PHPFileReflectionFunctionIterator.php
+++ b/src/PHPFileReflectionFunctionIterator.php
@@ -4,10 +4,12 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use Closure;
 use Generator;
 use IteratorAggregate;
 use LogicException;
 use ReflectionFunction;
+use RuntimeException;
 use SplFileInfo;
 
 /**
@@ -50,7 +52,13 @@ readonly class PHPFileReflectionFunctionIterator implements IteratorAggregate
      */
     private function assertFunctionExists(SplFileInfo $file, string $functionName): string
     {
-        require_once $file->getPathname();
+        $requireClosure = Closure::fromCallable(function (SplFileInfo $file) {
+            require_once $file->getPathname();
+        })->bindTo(null);
+
+        if ($requireClosure) {
+            $requireClosure($file);
+        }
 
         if (function_exists($functionName)) {
             return $functionName;
@@ -80,6 +88,8 @@ readonly class PHPFileReflectionFunctionIterator implements IteratorAggregate
             return null;
         }
 
+        $fileBasenameWithoutExtension = $file->getBasename('.php');
+
         $namespaceSequence = new PHPTokenSequenceMatcher([
             'T_WHITESPACE',
             'T_FUNCTION',
@@ -98,9 +108,16 @@ readonly class PHPFileReflectionFunctionIterator implements IteratorAggregate
             $namespaceSequence->pushToken($token);
 
             if ($namespaceSequence->isMatching()) {
-                $functionName = $namespace.'\\'.$namespaceSequence->matchingTokens->get(3)->text;
+                $baseFunctionName = $namespaceSequence->matchingTokens->get(3)->text;
+                $functionName = $namespace.'\\'.$baseFunctionName;
 
-                return $this->assertFunctionExists($file, $functionName);
+                if ($fileBasenameWithoutExtension !== $baseFunctionName && strtolower($baseFunctionName) === strtolower($fileBasenameWithoutExtension)) {
+                    throw new RuntimeException('Function name must match the file name: '.$file->getPathname());
+                }
+
+                if ($fileBasenameWithoutExtension === $baseFunctionName) {
+                    return $this->assertFunctionExists($file, $functionName);
+                }
             }
         }
 
diff --git a/src/PromptSubjectRequest.php b/src/PromptSubjectRequest.php
index 7815b11597343561cfd967162e899110961ab81c..1af1fe87315e572dae0457f05ee3cf29d2449f6c 100644
--- a/src/PromptSubjectRequest.php
+++ b/src/PromptSubjectRequest.php
@@ -11,5 +11,6 @@ readonly class PromptSubjectRequest
 {
     public function __construct(
         public ?AuthenticatedUser $authenticatedUser,
+        public string $prompt,
     ) {}
 }
diff --git a/src/PromptSubjectResponderAggregate.php b/src/PromptSubjectResponderAggregate.php
index 5193006f1e67ea67857f1d7ff72307e5d2846025..77beb6b5dc7b6b06a3a65d5469bec74bf25a9a49 100644
--- a/src/PromptSubjectResponderAggregate.php
+++ b/src/PromptSubjectResponderAggregate.php
@@ -8,6 +8,8 @@ use Distantmagic\Resonance\Attribute\Singleton;
 use Generator;
 use Psr\Log\LoggerInterface;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGo;
+
 #[Singleton]
 readonly class PromptSubjectResponderAggregate
 {
@@ -25,8 +27,10 @@ readonly class PromptSubjectResponderAggregate
         float $inactivityTimeout = DM_POOL_CONNECTION_TIMEOUT,
     ): Generator {
         $subjectActionTokenReader = new SubjectActionTokenReader();
+        $prompt = '';
 
         foreach ($completion as $token) {
+            $prompt .= (string) $token;
             $subjectActionTokenReader->write($token);
 
             if ($subjectActionTokenReader->isUnknown()) {
@@ -35,7 +39,7 @@ readonly class PromptSubjectResponderAggregate
                 break;
             }
 
-            if ($token->isFailed) {
+            if ($token->isFailed()) {
                 yield new PromptSubjectResponseChunk(
                     isFailed: true,
                     isLastChunk: true,
@@ -55,6 +59,7 @@ readonly class PromptSubjectResponderAggregate
         if ($subjectActionTokenReader->isUnknown() || !isset($action, $subject)) {
             yield from $this->respondWithSubjectAction(
                 $authenticatedUser,
+                $prompt,
                 'unknown',
                 'unknown',
                 $inactivityTimeout,
@@ -62,6 +67,7 @@ readonly class PromptSubjectResponderAggregate
         } else {
             yield from $this->respondWithSubjectAction(
                 $authenticatedUser,
+                $prompt,
                 $subject,
                 $action,
                 $inactivityTimeout,
@@ -77,6 +83,7 @@ readonly class PromptSubjectResponderAggregate
      */
     private function respondWithSubjectAction(
         ?AuthenticatedUser $authenticatedUser,
+        string $prompt,
         string $subject,
         string $action,
         float $inactivityTimeout,
@@ -98,10 +105,13 @@ readonly class PromptSubjectResponderAggregate
             return;
         }
 
-        $request = new PromptSubjectRequest($authenticatedUser);
+        $request = new PromptSubjectRequest(
+            authenticatedUser: $authenticatedUser,
+            prompt: $prompt,
+        );
         $response = new PromptSubjectResponse($inactivityTimeout);
 
-        SwooleCoroutineHelper::mustGo(static function () use ($request, $responder, $response) {
+        coroutineMustGo(static function () use ($request, $responder, $response) {
             $responder->respondToPromptSubject($request, $response);
         });
 
diff --git a/src/SwooleContextRequestResponseReader.php b/src/SwooleContextRequestResponseReader.php
index f77c7fc96948cc26a4473d75a7ffafe7995743d6..f25e44aa0c4bc51c9dd40fe2180cb2810c609b95 100644
--- a/src/SwooleContextRequestResponseReader.php
+++ b/src/SwooleContextRequestResponseReader.php
@@ -8,6 +8,8 @@ use Assert\Assertion;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 
+use function Distantmagic\Resonance\helpers\coroutineMustGetContext;
+
 readonly class SwooleContextRequestResponseReader
 {
     public const CONTEXT_KEY_REQUEST = 'psr_http_request';
@@ -27,7 +29,7 @@ readonly class SwooleContextRequestResponseReader
             return;
         }
 
-        $context = SwooleCoroutineHelper::mustGetContext();
+        $context = coroutineMustGetContext();
 
         /**
          * @var mixed explicitly mixed for typechecks
diff --git a/src/SwooleCoroutineHelper.php b/src/SwooleCoroutineHelper.php
deleted file mode 100644
index 0ce6a61fa135277caef15d6b83b0edb315692ed4..0000000000000000000000000000000000000000
--- a/src/SwooleCoroutineHelper.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Distantmagic\Resonance;
-
-use RuntimeException;
-use Swoole\Coroutine;
-use Swoole\Coroutine\Context;
-use Throwable;
-
-use function Swoole\Coroutine\go;
-use function Swoole\Coroutine\run;
-
-final readonly class SwooleCoroutineHelper
-{
-    public static function mustGetContext(): Context
-    {
-        /**
-         * @var null|Context
-         */
-        $context = Coroutine::getContext();
-
-        if (is_null($context)) {
-            throw new RuntimeException('Unable to get coroutine context');
-        }
-
-        return $context;
-    }
-
-    /**
-     * @param callable() $callback
-     */
-    public static function mustGo(callable $callback): int
-    {
-        /**
-         * @var false|int $cid
-         */
-        $cid = go($callback);
-
-        if (!is_int($cid)) {
-            throw new RuntimeException('Unable to start a coroutine');
-        }
-
-        return $cid;
-    }
-
-    /**
-     * @template TReturn
-     *
-     * @param callable():TReturn $callback
-     *
-     * @return TReturn
-     */
-    public static function mustRun(callable $callback): mixed
-    {
-        /**
-         * @var null|TReturn $ret
-         */
-        $ret = null;
-
-        /**
-         * Bringing this reference out of the coroutine event loops allows the
-         * console component to catch that exception and format it.
-         *
-         * @var null|Throwable
-         */
-        $exception = null;
-
-        /**
-         * @var bool
-         */
-        $coroutineResult = run(static function () use ($callback, &$exception, &$ret): void {
-            try {
-                $ret = $callback();
-            } catch (Throwable $throwable) {
-                $exception = $throwable;
-            }
-        });
-
-        if (!$coroutineResult) {
-            throw new RuntimeException('Unable to start coroutine loop');
-        }
-
-        if ($exception) {
-            throw $exception;
-        }
-
-        /**
-         * @var TReturn might also be null, so no way to check for that
-         */
-        return $ret;
-    }
-}
diff --git a/src/SwooleTimeoutScheduled.php b/src/SwooleTimeoutScheduled.php
index d1937073d6aed089886a3a05ada351913ace343f..9cf7317a7c8faa4d4d18bb54f6705c7aeb2ee3a7 100644
--- a/src/SwooleTimeoutScheduled.php
+++ b/src/SwooleTimeoutScheduled.php
@@ -26,6 +26,9 @@ readonly class SwooleTimeoutScheduled
         return Timer::clear($this->timeoutId);
     }
 
+    /**
+     * @psalm-suppress PossiblyUnusedReturnValue used in apps
+     */
     public function reschedule(float $timeout): ?self
     {
         if (!$this->cancel()) {
diff --git a/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php b/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
index 3b70ccbd6e2c38d38169d5b1f102a475a130b6ff..63fab044980e16d529e3adb5cc29524f1d276531 100644
--- a/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
+++ b/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
@@ -12,8 +12,8 @@ use Distantmagic\Resonance\LlamaCppCompletionRequest;
 use Distantmagic\Resonance\LlmChatHistory;
 use Distantmagic\Resonance\LlmChatMessage;
 use Distantmagic\Resonance\LlmPrompt\SubjectActionPrompt;
+use Distantmagic\Resonance\ObservableTask;
 use Distantmagic\Resonance\ObservableTaskCategory;
-use Distantmagic\Resonance\ObservableTaskFactory;
 use Distantmagic\Resonance\ObservableTaskStatus;
 use Distantmagic\Resonance\ObservableTaskStatusUpdate;
 use Distantmagic\Resonance\ObservableTaskTable;
@@ -84,7 +84,7 @@ abstract readonly class LlamaCppSubjectActionPromptResponder extends WebSocketJs
         WebSocketConnection $webSocketConnection,
         JsonRPCRequest $rpcRequest,
     ): void {
-        $this->observableTaskTable->observe(ObservableTaskFactory::withTimeout(
+        $this->observableTaskTable->observe(new ObservableTask(
             iterableTask: function () use (
                 $webSocketAuthResolution,
                 $webSocketConnection,
@@ -96,7 +96,6 @@ abstract readonly class LlamaCppSubjectActionPromptResponder extends WebSocketJs
                     $rpcRequest,
                 );
             },
-            inactivityTimeout: 5.0,
             name: 'websocket_jsonrpc_response',
             category: ObservableTaskCategory::LlamaCpp->value,
         ));
diff --git a/src/helpers/coroutineMustGetContext.php b/src/helpers/coroutineMustGetContext.php
new file mode 100644
index 0000000000000000000000000000000000000000..25d969cfba7686bbf709f425f41275acfd889a11
--- /dev/null
+++ b/src/helpers/coroutineMustGetContext.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\helpers;
+
+use RuntimeException;
+use Swoole\Coroutine;
+use Swoole\Coroutine\Context;
+
+function coroutineMustGetContext(): Context
+{
+    /**
+     * @var null|Context
+     */
+    $context = Coroutine::getContext();
+
+    if (is_null($context)) {
+        throw new RuntimeException('Unable to get coroutine context');
+    }
+
+    return $context;
+}
diff --git a/src/helpers/coroutineMustGo.php b/src/helpers/coroutineMustGo.php
new file mode 100644
index 0000000000000000000000000000000000000000..1519b87156350da569ecaf75873fedadb5106a1c
--- /dev/null
+++ b/src/helpers/coroutineMustGo.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\helpers;
+
+use RuntimeException;
+
+/**
+ * @param callable() $callback
+ */
+function coroutineMustGo(callable $callback): int
+{
+    $cid = go($callback);
+
+    if (!is_int($cid)) {
+        throw new RuntimeException('Unable to start a coroutine');
+    }
+
+    return $cid;
+}
diff --git a/src/helpers/coroutineMustRun.php b/src/helpers/coroutineMustRun.php
new file mode 100644
index 0000000000000000000000000000000000000000..dace96dc4618737acedf79672d60e06ca322d33f
--- /dev/null
+++ b/src/helpers/coroutineMustRun.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\helpers;
+
+use RuntimeException;
+
+use Throwable;
+
+use function Swoole\Coroutine\run;
+
+/**
+ * @template TReturn
+ *
+ * @param callable():TReturn $callback
+ *
+ * @return TReturn
+ */
+function coroutineMustRun(callable $callback): mixed
+{
+    /**
+     * @var null|TReturn $ret
+     */
+    $ret = null;
+
+    /**
+     * Bringing this reference out of the coroutine event loops allows the
+     * console component to catch that exception and format it.
+     *
+     * @var null|Throwable
+     */
+    $exception = null;
+
+    /**
+     * @var bool
+     */
+    $coroutineResult = run(static function () use ($callback, &$exception, &$ret): void {
+        try {
+            $ret = $callback();
+        } catch (Throwable $throwable) {
+            $exception = $throwable;
+        }
+    });
+
+    if (!$coroutineResult) {
+        throw new RuntimeException('Unable to start coroutine loop');
+    }
+
+    if ($exception) {
+        throw $exception;
+    }
+
+    /**
+     * @var TReturn might also be null, so no way to check for that
+     */
+    return $ret;
+}
diff --git a/src/helpers/generatorGetReturn.php b/src/helpers/generatorGetReturn.php
new file mode 100644
index 0000000000000000000000000000000000000000..a269ba5b83d557c7c8e816f801320fc9561e439e
--- /dev/null
+++ b/src/helpers/generatorGetReturn.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\helpers;
+
+use Generator;
+
+/**
+ * @psalm-suppress UnusedForeachValue we need to consume the iterator
+ *
+ * @template TReturn
+ *
+ * @param Generator<mixed,mixed,mixed,TReturn> $generator
+ *
+ * @return TReturn
+ */
+function generatorGetReturn(Generator $generator): mixed
+{
+    /**
+     * @var mixed $value explicitly mixed for typechecks
+     */
+    foreach ($generator as $value) {
+    }
+
+    return $generator->getReturn();
+}
diff --git a/tools/php-cs-fixer/composer.lock b/tools/php-cs-fixer/composer.lock
index 00d221ec872476f45f490c7f77cd1a69e7508577..658ff70337e86b9d0c3c5822e8559e9431a5b923 100644
--- a/tools/php-cs-fixer/composer.lock
+++ b/tools/php-cs-fixer/composer.lock
@@ -226,16 +226,16 @@
         },
         {
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.52.1",
+            "version": "v3.54.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
-                "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc"
+                "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc",
-                "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc",
+                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2aecbc8640d7906c38777b3dcab6f4ca79004d08",
+                "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08",
                 "shasum": ""
             },
             "require": {
@@ -259,6 +259,7 @@
             },
             "require-dev": {
                 "facile-it/paraunit": "^1.3 || ^2.0",
+                "infection/infection": "^0.27.11",
                 "justinrainbow/json-schema": "^5.2",
                 "keradus/cli-executor": "^2.1",
                 "mikey179/vfsstream": "^1.6.11",
@@ -306,7 +307,7 @@
             ],
             "support": {
                 "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
-                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1"
+                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.54.0"
             },
             "funding": [
                 {
@@ -314,7 +315,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-03-19T21:02:43+00:00"
+            "time": "2024-04-17T08:12:13+00:00"
         },
         {
             "name": "psr/container",
diff --git a/tools/psalm/composer.lock b/tools/psalm/composer.lock
index d4c71d210c6ca5ad07a4d520d52088f12cc2d7b4..3a616aa7c8d4c401037695ae3deae4ac71b2ade3 100644
--- a/tools/psalm/composer.lock
+++ b/tools/psalm/composer.lock
@@ -97,16 +97,16 @@
         },
         {
             "name": "amphp/byte-stream",
-            "version": "v1.8.1",
+            "version": "v1.8.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/amphp/byte-stream.git",
-                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
+                "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
-                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
+                "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc",
+                "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc",
                 "shasum": ""
             },
             "require": {
@@ -122,11 +122,6 @@
                 "psalm/phar": "^3.11.4"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
             "autoload": {
                 "files": [
                     "lib/functions.php"
@@ -150,7 +145,7 @@
                 }
             ],
             "description": "A stream abstraction to make working with non-blocking I/O simple.",
-            "homepage": "http://amphp.org/byte-stream",
+            "homepage": "https://amphp.org/byte-stream",
             "keywords": [
                 "amp",
                 "amphp",
@@ -160,9 +155,8 @@
                 "stream"
             ],
             "support": {
-                "irc": "irc://irc.freenode.org/amphp",
                 "issues": "https://github.com/amphp/byte-stream/issues",
-                "source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
+                "source": "https://github.com/amphp/byte-stream/tree/v1.8.2"
             },
             "funding": [
                 {
@@ -170,7 +164,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-03-30T17:13:30+00:00"
+            "time": "2024-04-13T18:00:56+00:00"
         },
         {
             "name": "composer/pcre",
@@ -798,28 +792,35 @@
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "5.3.0",
+            "version": "5.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
+                "reference": "298d2febfe79d03fe714eb871d5538da55205b1a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
-                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a",
+                "reference": "298d2febfe79d03fe714eb871d5538da55205b1a",
                 "shasum": ""
             },
             "require": {
+                "doctrine/deprecations": "^1.1",
                 "ext-filter": "*",
-                "php": "^7.2 || ^8.0",
+                "php": "^7.4 || ^8.0",
                 "phpdocumentor/reflection-common": "^2.2",
-                "phpdocumentor/type-resolver": "^1.3",
+                "phpdocumentor/type-resolver": "^1.7",
+                "phpstan/phpdoc-parser": "^1.7",
                 "webmozart/assert": "^1.9.1"
             },
             "require-dev": {
-                "mockery/mockery": "~1.3.2",
-                "psalm/phar": "^4.8"
+                "mockery/mockery": "~1.3.5",
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1.8",
+                "phpstan/phpstan-mockery": "^1.1",
+                "phpstan/phpstan-webmozart-assert": "^1.2",
+                "phpunit/phpunit": "^9.5",
+                "vimeo/psalm": "^5.13"
             },
             "type": "library",
             "extra": {
@@ -843,15 +844,15 @@
                 },
                 {
                     "name": "Jaap van Otterdijk",
-                    "email": "account@ijaap.nl"
+                    "email": "opensource@ijaap.nl"
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
             "support": {
                 "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
-                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
+                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0"
             },
-            "time": "2021-10-19T17:43:47+00:00"
+            "time": "2024-04-09T21:13:58+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",