diff --git a/composer.json b/composer.json
index 7a5d9ed0e57c18e7e453705cd35ad97fba2dac89..5e96603c5032d3093abaea88d84e8821ee7343de 100644
--- a/composer.json
+++ b/composer.json
@@ -26,6 +26,7 @@
         "dragonmantank/cron-expression": "^3.3",
         "ezyang/htmlpurifier": "^4.16",
         "grpc/grpc": "^1.57",
+        "hyperf/coroutine": "^3.1",
         "hyperf/grpc-client": "^3.1",
         "league/commonmark": "^2.4",
         "league/oauth2-client": "^2.7",
@@ -60,7 +61,8 @@
     "require-dev": {
         "phpunit/phpunit": "^11.0",
         "swoole/ide-helper": "^5.1",
-        "symfony/var-dumper": "^7.0"
+        "symfony/var-dumper": "^7.0",
+        "mockery/mockery": "^1.6"
     },
     "suggest": {
         "ext-ds": "For better memory management",
diff --git a/composer.lock b/composer.lock
index 2de4e9571e7e0415e503196407c93fd849d6c752..0cafaf8810ae2e031c6f9e226b430e60cef595b3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "6cc5c7a01d9bb04a890e15a3e326d795",
+    "content-hash": "2e42788f572c165ae18374fbd867a9b7",
     "packages": [
         {
             "name": "brick/math",
@@ -1841,16 +1841,16 @@
         },
         {
             "name": "hyperf/code-parser",
-            "version": "v3.1.4",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/code-parser.git",
-                "reference": "e36ac337cf7852aaa817db61ade0941d8826f0d8"
+                "reference": "820e8e6680f0d04e4187a3037a2a3eaf7141913d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/code-parser/zipball/e36ac337cf7852aaa817db61ade0941d8826f0d8",
-                "reference": "e36ac337cf7852aaa817db61ade0941d8826f0d8",
+                "url": "https://api.github.com/repos/hyperf/code-parser/zipball/820e8e6680f0d04e4187a3037a2a3eaf7141913d",
+                "reference": "820e8e6680f0d04e4187a3037a2a3eaf7141913d",
                 "shasum": ""
             },
             "require": {
@@ -1902,20 +1902,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-12-28T08:46:40+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/collection",
-            "version": "v3.1.7",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/collection.git",
-                "reference": "40e25ca34f798e173a1f9a6871d56a6abae43dbb"
+                "reference": "d0ac957987a704c8b2a16de4333b81f1f56a724a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/collection/zipball/40e25ca34f798e173a1f9a6871d56a6abae43dbb",
-                "reference": "40e25ca34f798e173a1f9a6871d56a6abae43dbb",
+                "url": "https://api.github.com/repos/hyperf/collection/zipball/d0ac957987a704c8b2a16de4333b81f1f56a724a",
+                "reference": "d0ac957987a704c8b2a16de4333b81f1f56a724a",
                 "shasum": ""
             },
             "require": {
@@ -1965,20 +1965,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-01-26T01:52:03+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/conditionable",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/conditionable.git",
-                "reference": "18da1405ae39a775bd3fae8cec98841eaa22f013"
+                "reference": "2c1555891d136904b890ba6d812d9ff50ca13ae3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/conditionable/zipball/18da1405ae39a775bd3fae8cec98841eaa22f013",
-                "reference": "18da1405ae39a775bd3fae8cec98841eaa22f013",
+                "url": "https://api.github.com/repos/hyperf/conditionable/zipball/2c1555891d136904b890ba6d812d9ff50ca13ae3",
+                "reference": "2c1555891d136904b890ba6d812d9ff50ca13ae3",
                 "shasum": ""
             },
             "require": {
@@ -2023,20 +2023,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/context",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/context.git",
-                "reference": "8307d6c924ed67c7abd47874ec14f0e2e3e4b732"
+                "reference": "ad913fd50eb5f738c038e172c120bc6956c0da69"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/context/zipball/8307d6c924ed67c7abd47874ec14f0e2e3e4b732",
-                "reference": "8307d6c924ed67c7abd47874ec14f0e2e3e4b732",
+                "url": "https://api.github.com/repos/hyperf/context/zipball/ad913fd50eb5f738c038e172c120bc6956c0da69",
+                "reference": "ad913fd50eb5f738c038e172c120bc6956c0da69",
                 "shasum": ""
             },
             "require": {
@@ -2085,20 +2085,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/contract",
-            "version": "v3.1.2",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/contract.git",
-                "reference": "f5379df6df65363d645506f373888372135ac0c6"
+                "reference": "9950abe963cc6b30c6d3506fa5b3adbd58cb1945"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/contract/zipball/f5379df6df65363d645506f373888372135ac0c6",
-                "reference": "f5379df6df65363d645506f373888372135ac0c6",
+                "url": "https://api.github.com/repos/hyperf/contract/zipball/9950abe963cc6b30c6d3506fa5b3adbd58cb1945",
+                "reference": "9950abe963cc6b30c6d3506fa5b3adbd58cb1945",
                 "shasum": ""
             },
             "require": {
@@ -2142,20 +2142,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-12-11T03:14:01+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/coroutine",
-            "version": "v3.1.1",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/coroutine.git",
-                "reference": "cd5bad67724c5c7a7ad749d8e9eb045470488d75"
+                "reference": "8f4c573a9457646db3e629dacabe064eebaf8cc1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/coroutine/zipball/cd5bad67724c5c7a7ad749d8e9eb045470488d75",
-                "reference": "cd5bad67724c5c7a7ad749d8e9eb045470488d75",
+                "url": "https://api.github.com/repos/hyperf/coroutine/zipball/8f4c573a9457646db3e629dacabe064eebaf8cc1",
+                "reference": "8f4c573a9457646db3e629dacabe064eebaf8cc1",
                 "shasum": ""
             },
             "require": {
@@ -2206,7 +2206,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-12-01T06:59:45+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/engine",
@@ -2357,16 +2357,16 @@
         },
         {
             "name": "hyperf/event",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/event.git",
-                "reference": "6eada5f74ce80786c567d5aed0361d51175217bb"
+                "reference": "8d008682c028e958197589e90232bb2a1d3c77d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/event/zipball/6eada5f74ce80786c567d5aed0361d51175217bb",
-                "reference": "6eada5f74ce80786c567d5aed0361d51175217bb",
+                "url": "https://api.github.com/repos/hyperf/event/zipball/8d008682c028e958197589e90232bb2a1d3c77d9",
+                "reference": "8d008682c028e958197589e90232bb2a1d3c77d9",
                 "shasum": ""
             },
             "require": {
@@ -2420,20 +2420,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/grpc",
-            "version": "v3.1.11",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/grpc.git",
-                "reference": "63d8c8aac9a9fd7e586aa1298d9928107e6e0e27"
+                "reference": "7b0424ce1b9af5016e2c8580d6de9eb9c6ec4bed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/grpc/zipball/63d8c8aac9a9fd7e586aa1298d9928107e6e0e27",
-                "reference": "63d8c8aac9a9fd7e586aa1298d9928107e6e0e27",
+                "url": "https://api.github.com/repos/hyperf/grpc/zipball/7b0424ce1b9af5016e2c8580d6de9eb9c6ec4bed",
+                "reference": "7b0424ce1b9af5016e2c8580d6de9eb9c6ec4bed",
                 "shasum": ""
             },
             "require": {
@@ -2483,20 +2483,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-02-28T03:13:03+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/grpc-client",
-            "version": "v3.1.7",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/grpc-client.git",
-                "reference": "ce4cbf105f5f890f43bdd1997b9d69d4d09f5549"
+                "reference": "4f8f1aeef090d7789020e87a058ed35dcf3fc127"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/grpc-client/zipball/ce4cbf105f5f890f43bdd1997b9d69d4d09f5549",
-                "reference": "ce4cbf105f5f890f43bdd1997b9d69d4d09f5549",
+                "url": "https://api.github.com/repos/hyperf/grpc-client/zipball/4f8f1aeef090d7789020e87a058ed35dcf3fc127",
+                "reference": "4f8f1aeef090d7789020e87a058ed35dcf3fc127",
                 "shasum": ""
             },
             "require": {
@@ -2555,20 +2555,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-01-22T08:45:43+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/macroable",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/macroable.git",
-                "reference": "de5b07be74d666f04ecef4ce5ee6ceb97d846cfa"
+                "reference": "8912b5de69d25451b8ca103e4e47f0935e81072b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/macroable/zipball/de5b07be74d666f04ecef4ce5ee6ceb97d846cfa",
-                "reference": "de5b07be74d666f04ecef4ce5ee6ceb97d846cfa",
+                "url": "https://api.github.com/repos/hyperf/macroable/zipball/8912b5de69d25451b8ca103e4e47f0935e81072b",
+                "reference": "8912b5de69d25451b8ca103e4e47f0935e81072b",
                 "shasum": ""
             },
             "require": {
@@ -2613,20 +2613,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/stdlib",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/stdlib.git",
-                "reference": "25b73da235551d0d71d9157324709abaea36c455"
+                "reference": "636fdc1f15d9357b4747fa649874725f2276b118"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/stdlib/zipball/25b73da235551d0d71d9157324709abaea36c455",
-                "reference": "25b73da235551d0d71d9157324709abaea36c455",
+                "url": "https://api.github.com/repos/hyperf/stdlib/zipball/636fdc1f15d9357b4747fa649874725f2276b118",
+                "reference": "636fdc1f15d9357b4747fa649874725f2276b118",
                 "shasum": ""
             },
             "require": {
@@ -2671,20 +2671,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/stringable",
-            "version": "v3.1.13",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/stringable.git",
-                "reference": "b79f7204c35874a4ea0e917239b74fd230c93e1f"
+                "reference": "6cbd6f220d833c3f2c28c8263dccffee48021dcb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/stringable/zipball/b79f7204c35874a4ea0e917239b74fd230c93e1f",
-                "reference": "b79f7204c35874a4ea0e917239b74fd230c93e1f",
+                "url": "https://api.github.com/repos/hyperf/stringable/zipball/6cbd6f220d833c3f2c28c8263dccffee48021dcb",
+                "reference": "6cbd6f220d833c3f2c28c8263dccffee48021dcb",
                 "shasum": ""
             },
             "require": {
@@ -2742,20 +2742,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-03-10T09:19:40+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/support",
-            "version": "v3.1.13",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/support.git",
-                "reference": "cd5d284d83d0c031be38e3c5d07f6b96b837a42f"
+                "reference": "3fb5c6c5a4f795cb0304a032f6f5b85f62a5f872"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/support/zipball/cd5d284d83d0c031be38e3c5d07f6b96b837a42f",
-                "reference": "cd5d284d83d0c031be38e3c5d07f6b96b837a42f",
+                "url": "https://api.github.com/repos/hyperf/support/zipball/3fb5c6c5a4f795cb0304a032f6f5b85f62a5f872",
+                "reference": "3fb5c6c5a4f795cb0304a032f6f5b85f62a5f872",
                 "shasum": ""
             },
             "require": {
@@ -2812,20 +2812,20 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2024-03-13T08:47:07+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "hyperf/tappable",
-            "version": "v3.1.0",
+            "version": "v3.1.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/hyperf/tappable.git",
-                "reference": "f640e37006dad09ca6f2b9a4cf047907aaebf002"
+                "reference": "69f22bbc8ecb5b930cc95a49ae9bf0ca0efbfdf1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hyperf/tappable/zipball/f640e37006dad09ca6f2b9a4cf047907aaebf002",
-                "reference": "f640e37006dad09ca6f2b9a4cf047907aaebf002",
+                "url": "https://api.github.com/repos/hyperf/tappable/zipball/69f22bbc8ecb5b930cc95a49ae9bf0ca0efbfdf1",
+                "reference": "69f22bbc8ecb5b930cc95a49ae9bf0ca0efbfdf1",
                 "shasum": ""
             },
             "require": {
@@ -2873,7 +2873,7 @@
                     "type": "open_collective"
                 }
             ],
-            "time": "2023-11-24T03:10:53+00:00"
+            "time": "2024-03-23T11:28:51+00:00"
         },
         {
             "name": "jean85/pretty-package-versions",
@@ -7667,6 +7667,140 @@
         }
     ],
     "packages-dev": [
+        {
+            "name": "hamcrest/hamcrest-php",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hamcrest/hamcrest-php.git",
+                "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
+                "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3|^7.0|^8.0"
+            },
+            "replace": {
+                "cordoval/hamcrest-php": "*",
+                "davedevelopment/hamcrest-php": "*",
+                "kodova/hamcrest-php": "*"
+            },
+            "require-dev": {
+                "phpunit/php-file-iterator": "^1.4 || ^2.0",
+                "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "hamcrest"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "This is the PHP port of Hamcrest Matchers",
+            "keywords": [
+                "test"
+            ],
+            "support": {
+                "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+                "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
+            },
+            "time": "2020-07-09T08:09:16+00:00"
+        },
+        {
+            "name": "mockery/mockery",
+            "version": "1.6.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mockery/mockery.git",
+                "reference": "81a161d0b135df89951abd52296adf97deb0723d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/81a161d0b135df89951abd52296adf97deb0723d",
+                "reference": "81a161d0b135df89951abd52296adf97deb0723d",
+                "shasum": ""
+            },
+            "require": {
+                "hamcrest/hamcrest-php": "^2.0.1",
+                "lib-pcre": ">=7.0",
+                "php": ">=7.3"
+            },
+            "conflict": {
+                "phpunit/phpunit": "<8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5 || ^9.6.17",
+                "symplify/easy-coding-standard": "^12.1.14"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "library/helpers.php",
+                    "library/Mockery.php"
+                ],
+                "psr-4": {
+                    "Mockery\\": "library/Mockery"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Pádraic Brady",
+                    "email": "padraic.brady@gmail.com",
+                    "homepage": "https://github.com/padraic",
+                    "role": "Author"
+                },
+                {
+                    "name": "Dave Marshall",
+                    "email": "dave.marshall@atstsolutions.co.uk",
+                    "homepage": "https://davedevelopment.co.uk",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Nathanael Esayeas",
+                    "email": "nathanael.esayeas@protonmail.com",
+                    "homepage": "https://github.com/ghostwriter",
+                    "role": "Lead Developer"
+                }
+            ],
+            "description": "Mockery is a simple yet flexible PHP mock object framework",
+            "homepage": "https://github.com/mockery/mockery",
+            "keywords": [
+                "BDD",
+                "TDD",
+                "library",
+                "mock",
+                "mock objects",
+                "mockery",
+                "stub",
+                "test",
+                "test double",
+                "testing"
+            ],
+            "support": {
+                "docs": "https://docs.mockery.io/",
+                "issues": "https://github.com/mockery/mockery/issues",
+                "rss": "https://github.com/mockery/mockery/releases.atom",
+                "security": "https://github.com/mockery/mockery/security/advisories",
+                "source": "https://github.com/mockery/mockery"
+            },
+            "time": "2024-03-21T18:34:15+00:00"
+        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.11.1",
@@ -8227,16 +8361,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "11.0.8",
+            "version": "11.0.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "48ea58408879a9aad630022186398364051482fc"
+                "reference": "591bbfe416400385527d5086b346b92c06de404b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/48ea58408879a9aad630022186398364051482fc",
-                "reference": "48ea58408879a9aad630022186398364051482fc",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/591bbfe416400385527d5086b346b92c06de404b",
+                "reference": "591bbfe416400385527d5086b346b92c06de404b",
                 "shasum": ""
             },
             "require": {
@@ -8307,7 +8441,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.0.8"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.9"
             },
             "funding": [
                 {
@@ -8323,7 +8457,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-03-22T04:21:01+00:00"
+            "time": "2024-03-28T10:09:42+00:00"
         },
         {
             "name": "sebastian/cli-parser",
diff --git a/docs/pages/docs/features/ai/server/llama-cpp/index.md b/docs/pages/docs/features/ai/server/llama-cpp/index.md
index 88047a59637dd22adad9538ef1fe8cc10d2f02ea..30f63337e76594c3d819d242cebf1197bb7d4ad4 100644
--- a/docs/pages/docs/features/ai/server/llama-cpp/index.md
+++ b/docs/pages/docs/features/ai/server/llama-cpp/index.md
@@ -46,14 +46,14 @@ you are serving. In the following example we will use
 
 namespace App;
 
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppCompletionRequest;
 use Distantmagic\Resonance\LlamaCppPromptTemplate\MistralInstructChat;
 
 #[Singleton]
 class LlamaCppGenerate 
 {
-    public function __construct(protected LlamaCppClient $llamaCppClient) 
+    public function __construct(protected LlamaCppClientInterface $llamaCppClient) 
     {
     }
 
@@ -104,13 +104,13 @@ foreach ($completion as $token) {
 
 namespace App;
 
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppEmbeddingRequest;
 
 #[Singleton]
 class LlamaCppGenerate 
 {
-    public function __construct(protected LlamaCppClient $llamaCppClient) 
+    public function __construct(protected LlamaCppClientInterface $llamaCppClient) 
     {
     }
 
diff --git a/docs/pages/index.md b/docs/pages/index.md
index 663aca8feb4911919921ff8eee97c9cbc2c816e0..b165868b4cd8c47a36a25b035233f72fee631d37 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -109,7 +109,7 @@ readonly class CatAdopt implements PromptSubjectResponderInterface
                         data-hljs-language-value="php"
                     >class LlamaCppGenerate 
 {
-    public function __construct(protected LlamaCppClient $llamaCppClient) 
+    public function __construct(protected LlamaCppClientInterface $llamaCppClient) 
     {
     }
 
diff --git a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
index a02b430c8e019498b0edb461f0e79a2ce9e93d1a..81406a4d8607ad23264e8fc432baafd235b05089 100644
--- a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
+++ b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
@@ -212,7 +212,7 @@ use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\JsonRPCNotification;
 use Distantmagic\Resonance\JsonRPCRequest;
 use Distantmagic\Resonance\JsonRPCResponse;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppCompletionRequest;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\WebSocketAuthResolution;
@@ -225,7 +225,7 @@ use Distantmagic\Resonance\WebSocketJsonRPCResponder;
 final readonly class LlmChatPromptResponder extends WebSocketJsonRPCResponder
 {
     public function __construct(
-        private LlamaCppClient $llamaCppClient,
+        private LlamaCppClientInterface $llamaCppClient,
     ) {}
 
     public function getConstraint(): Constraint
diff --git a/docs/pages/tutorials/how-to-serve-llm-completions/index.md b/docs/pages/tutorials/how-to-serve-llm-completions/index.md
index 4305e4a84ca91e4750a787961b2cab8638d4ac98..09a12072766c094d7109a912e967eceb6c796ef6 100644
--- a/docs/pages/tutorials/how-to-serve-llm-completions/index.md
+++ b/docs/pages/tutorials/how-to-serve-llm-completions/index.md
@@ -111,13 +111,13 @@ inject `LlamaCppClient`:
 
 namespace App;
 
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppCompletionRequest;
 
 #[Singleton]
 class LlamaCppGenerate 
 {
-    public function __construct(protected LlamaCppClient $llamaCppClient) 
+    public function __construct(protected LlamaCppClientInterface $llamaCppClient) 
     {
     }
 
diff --git a/phpunit.xml b/phpunit.xml
index f960f454e08bb40d9b235874c48330e16c67ef57..3658f46884ff0698c5d749df2ec34df1f1d9c6d3 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -2,7 +2,7 @@
 <phpunit
     beStrictAboutCoverageMetadata="true"
     beStrictAboutChangesToGlobalState="true"
-    beStrictAboutTestsThatDoNotTestAnything="false"
+    beStrictAboutTestsThatDoNotTestAnything="true"
     bootstrap="phpunit_bootstrap.php"
     colors="true"
     displayDetailsOnTestsThatTriggerWarnings="true"
diff --git a/src/Attribute/RespondsToHttp.php b/src/Attribute/RespondsToHttp.php
index 49890d77e3f4626c9a7f19c2360458b002914e1b..befd9cbe94ceee54b395c5643e305818b18c3660 100644
--- a/src/Attribute/RespondsToHttp.php
+++ b/src/Attribute/RespondsToHttp.php
@@ -9,7 +9,7 @@ use Distantmagic\Resonance\Attribute as BaseAttribute;
 use Distantmagic\Resonance\HttpRouteSymbolInterface;
 use Distantmagic\Resonance\RequestMethod;
 
-#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
+#[Attribute(Attribute::TARGET_CLASS)]
 final readonly class RespondsToHttp extends BaseAttribute
 {
     /**
diff --git a/src/Command/LlamaCppGenerate.php b/src/Command/LlamaCppGenerate.php
index 2fab6a39bbb45ec2bfacfbf71df945891ced0237..fe2468eebc56e923ba8bb2f025c4e61b476b2c68 100644
--- a/src/Command/LlamaCppGenerate.php
+++ b/src/Command/LlamaCppGenerate.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\Command;
 
 use Distantmagic\Resonance\CoroutineCommand;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\SwooleConfiguration;
 use RuntimeException;
 use Symfony\Component\Console\Input\InputArgument;
@@ -20,7 +20,7 @@ abstract class LlamaCppGenerate extends CoroutineCommand
     abstract protected function executeLlamaCppCommand(InputInterface $input, OutputInterface $output, string $prompt): int;
 
     public function __construct(
-        protected LlamaCppClient $llamaCppClient,
+        protected LlamaCppClientInterface $llamaCppClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($swooleConfiguration);
diff --git a/src/Command/LlamaCppGenerate/Embedding.php b/src/Command/LlamaCppGenerate/Embedding.php
index 7d5d376a939bfa13542dbd9f24a7d746b75a8ee7..3a9e0545478912adc8af50cbd50e2414911b8b5c 100644
--- a/src/Command/LlamaCppGenerate/Embedding.php
+++ b/src/Command/LlamaCppGenerate/Embedding.php
@@ -8,7 +8,7 @@ use Distantmagic\Resonance\Attribute\ConsoleCommand;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\Command\LlamaCppGenerate;
 use Distantmagic\Resonance\JsonSerializer;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppEmbeddingRequest;
 use Distantmagic\Resonance\SwooleConfiguration;
 use Symfony\Component\Console\Input\InputInterface;
@@ -22,7 +22,7 @@ final class Embedding extends LlamaCppGenerate
 {
     public function __construct(
         private readonly JsonSerializer $jsonSerializer,
-        LlamaCppClient $llamaCppClient,
+        LlamaCppClientInterface $llamaCppClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($llamaCppClient, $swooleConfiguration);
diff --git a/src/Command/LlamaCppHealth.php b/src/Command/LlamaCppHealth.php
index a5b64759345dbd66a2c677a9b9a8db3d8f1efbfe..388391c78b372137ffbf4884d7466eae5af7328b 100644
--- a/src/Command/LlamaCppHealth.php
+++ b/src/Command/LlamaCppHealth.php
@@ -7,7 +7,7 @@ namespace Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\Attribute\ConsoleCommand;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\CoroutineCommand;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\SwooleConfiguration;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -19,7 +19,7 @@ use Symfony\Component\Console\Output\OutputInterface;
 final class LlamaCppHealth extends CoroutineCommand
 {
     public function __construct(
-        private readonly LlamaCppClient $llamaCppClient,
+        private readonly LlamaCppClientInterface $llamaCppClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($swooleConfiguration);
diff --git a/src/Command/LlamaCppInfill.php b/src/Command/LlamaCppInfill.php
index 16f8908e4c793f9d355771b8a458abd1894a752b..ecb5222f97c100aeebdf78cc626d94eb3def4244 100644
--- a/src/Command/LlamaCppInfill.php
+++ b/src/Command/LlamaCppInfill.php
@@ -8,7 +8,7 @@ use Distantmagic\Resonance\Attribute\ConsoleCommand;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\CoroutineCommand;
 use Distantmagic\Resonance\JsonSerializer;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppInfillRequest;
 use Distantmagic\Resonance\SwooleConfiguration;
 use Symfony\Component\Console\Input\InputInterface;
@@ -22,7 +22,7 @@ final class LlamaCppInfill extends CoroutineCommand
 {
     public function __construct(
         private readonly JsonSerializer $jsonSerializer,
-        private readonly LlamaCppClient $llamaCppClient,
+        private readonly LlamaCppClientInterface $llamaCppClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($swooleConfiguration);
diff --git a/src/Command/StaticPagesMakeEmbeddings.php b/src/Command/StaticPagesMakeEmbeddings.php
index b948cfc605e9bcd2cc6e653cc07523a2b6e80d50..9b6f8ad18c3746fcdd38d0c50564baf5f78c7ac0 100644
--- a/src/Command/StaticPagesMakeEmbeddings.php
+++ b/src/Command/StaticPagesMakeEmbeddings.php
@@ -9,7 +9,7 @@ use Distantmagic\Resonance\Attribute\WantsFeature;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\JsonSerializer;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppEmbeddingRequest;
 use Distantmagic\Resonance\SQLiteVSSConnectionBuilder;
 use Distantmagic\Resonance\StaticPageChunkIterator;
@@ -29,7 +29,7 @@ final class StaticPagesMakeEmbeddings extends Command
 
     public function __construct(
         private readonly JsonSerializer $jsonSerializer,
-        private readonly LlamaCppClient $llamaCppClient,
+        private readonly LlamaCppClientInterface $llamaCppClient,
         private readonly SQLiteVSSConnectionBuilder $sqliteVSSConnectionBuilder,
         private readonly StaticPageChunkIterator $staticPageChunkIterator,
     ) {
diff --git a/src/DialogueInput.php b/src/DialogueInput.php
new file mode 100644
index 0000000000000000000000000000000000000000..f22e1acb9d6ac636ecb050a7c858201d6b149aaa
--- /dev/null
+++ b/src/DialogueInput.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+abstract readonly class DialogueInput implements DialogueInputInterface {}
diff --git a/src/DialogueInput/UserInput.php b/src/DialogueInput/UserInput.php
new file mode 100644
index 0000000000000000000000000000000000000000..10bcb293ac09ae9b0fb694fe5622a1be6cf517aa
--- /dev/null
+++ b/src/DialogueInput/UserInput.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueInput;
+
+use Distantmagic\Resonance\DialogueInputInterface;
+
+readonly class UserInput implements DialogueInputInterface
+{
+    public function __construct(
+        private string $content,
+    ) {}
+
+    public function getContent(): string
+    {
+        return $this->content;
+    }
+}
diff --git a/src/DialogueInputInterface.php b/src/DialogueInputInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..d68037a559704c22aa3f8a7f9c3e8a2d6bbfad52
--- /dev/null
+++ b/src/DialogueInputInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueInputInterface
+{
+    public function getContent(): string;
+}
diff --git a/src/DialogueMessageChunk.php b/src/DialogueMessageChunk.php
new file mode 100644
index 0000000000000000000000000000000000000000..7df4c9220d1dc48cebfe2518f44482f418774f6c
--- /dev/null
+++ b/src/DialogueMessageChunk.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Stringable;
+
+readonly class DialogueMessageChunk implements Stringable
+{
+    public function __construct(
+        public string $content,
+        public bool $isFailed,
+        public bool $isLastToken,
+    ) {}
+
+    public function __toString(): string
+    {
+        return $this->content;
+    }
+}
diff --git a/src/DialogueMessageProducer.php b/src/DialogueMessageProducer.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4cfacec725dcc0f33f05484a7dcd978e2ce9a10
--- /dev/null
+++ b/src/DialogueMessageProducer.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+abstract readonly class DialogueMessageProducer implements DialogueMessageProducerInterface {}
diff --git a/src/DialogueMessageProducer/ConstMessageProducer.php b/src/DialogueMessageProducer/ConstMessageProducer.php
new file mode 100644
index 0000000000000000000000000000000000000000..4d3a24c803c8596f4182d3a3bd885f3e4ea8fa5e
--- /dev/null
+++ b/src/DialogueMessageProducer/ConstMessageProducer.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueMessageProducer;
+
+use Distantmagic\Resonance\DialogueMessageChunk;
+use Distantmagic\Resonance\DialogueMessageProducer;
+use Generator;
+
+readonly class ConstMessageProducer extends DialogueMessageProducer
+{
+    public function __construct(
+        public string $content,
+    ) {}
+
+    /**
+     * @return Generator<DialogueMessageChunk>
+     */
+    public function getIterator(): Generator
+    {
+        yield new DialogueMessageChunk(
+            content: $this->content,
+            isFailed: false,
+            isLastToken: true,
+        );
+    }
+}
diff --git a/src/DialogueMessageProducer/ConstMessageProducerTest.php b/src/DialogueMessageProducer/ConstMessageProducerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bbae0441a3f8705afd279fdac7c9965f95fa7b84
--- /dev/null
+++ b/src/DialogueMessageProducer/ConstMessageProducerTest.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueMessageProducer;
+
+use Distantmagic\Resonance\DialogueMessageChunk;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @internal
+ */
+#[CoversClass(ConstMessageProducer::class)]
+final class ConstMessageProducerTest extends TestCase
+{
+    public function test_message_is_produced(): void
+    {
+        $inputMessage = 'What is your current role?';
+        $messageProducer = new ConstMessageProducer($inputMessage);
+
+        $message = '';
+
+        foreach ($messageProducer as $messageChunk) {
+            self::assertInstanceOf(DialogueMessageChunk::class, $messageChunk);
+            self::assertFalse($messageChunk->isFailed);
+            self::assertTrue($messageChunk->isLastToken);
+
+            $message .= $messageChunk->content;
+        }
+
+        self::assertSame($inputMessage, $message);
+    }
+}
diff --git a/src/DialogueMessageProducer/EmptyMessageProducer.php b/src/DialogueMessageProducer/EmptyMessageProducer.php
new file mode 100644
index 0000000000000000000000000000000000000000..b3f1fba312cda3496f58b79ca7f11c640feeeeb2
--- /dev/null
+++ b/src/DialogueMessageProducer/EmptyMessageProducer.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueMessageProducer;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\DialogueMessageChunk;
+use Distantmagic\Resonance\DialogueMessageProducer;
+use Generator;
+
+#[Singleton]
+readonly class EmptyMessageProducer extends DialogueMessageProducer
+{
+    public function getIterator(): Generator
+    {
+        yield new DialogueMessageChunk(
+            content: '',
+            isFailed: false,
+            isLastToken: true,
+        );
+    }
+}
diff --git a/src/DialogueMessageProducerInterface.php b/src/DialogueMessageProducerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..7cdfec891ea1e2d9135ddd97b33ce979d3060c2e
--- /dev/null
+++ b/src/DialogueMessageProducerInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use IteratorAggregate;
+
+/**
+ * @template-extends IteratorAggregate<DialogueMessageChunk>
+ */
+interface DialogueMessageProducerInterface extends IteratorAggregate {}
diff --git a/src/DialogueNode.php b/src/DialogueNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..5943acf3a137d3a3cf680a1ddb8d70e54e9ffcdf
--- /dev/null
+++ b/src/DialogueNode.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Ds\Set;
+
+readonly class DialogueNode implements DialogueNodeInterface
+{
+    /**
+     * @var Set<DialogueResponseInterface>
+     */
+    private Set $responses;
+
+    public function __construct(
+        private DialogueMessageProducerInterface $message,
+        private DialogueResponseDiscriminatorInterface $responseDiscriminator,
+    ) {
+        $this->responses = new Set();
+    }
+
+    public function addResponse(DialogueResponseInterface $response): void
+    {
+        $this->responses->add($response);
+    }
+
+    public function getMessageProducer(): DialogueMessageProducerInterface
+    {
+        return $this->message;
+    }
+
+    public function respondTo(DialogueInputInterface $prompt): ?DialogueNodeInterface
+    {
+        return $this->responseDiscriminator->discriminate($this->responses, $prompt);
+    }
+}
diff --git a/src/DialogueNodeInterface.php b/src/DialogueNodeInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..875b1c9a23c6864555003f7fc60ad349687e36f4
--- /dev/null
+++ b/src/DialogueNodeInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueNodeInterface
+{
+    public function addResponse(DialogueResponseInterface $response): void;
+
+    public function getMessageProducer(): DialogueMessageProducerInterface;
+
+    public function respondTo(DialogueInputInterface $prompt): ?self;
+}
diff --git a/src/DialogueNodeTest.php b/src/DialogueNodeTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..64b86b70cd5b0c92c2eabc53cf9274dd3d77c01c
--- /dev/null
+++ b/src/DialogueNodeTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\DialogueInput\UserInput;
+use Distantmagic\Resonance\DialogueMessageProducer\ConstMessageProducer;
+use Distantmagic\Resonance\DialogueResponseCondition\ExactInputCondition;
+use Distantmagic\Resonance\DialogueResponseCondition\LlamaCppInputCondition;
+use Mockery;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @internal
+ */
+#[CoversClass(DialogueNode::class)]
+final class DialogueNodeTest extends TestCase
+{
+    public function test_dialogue_produces_no_response(): void
+    {
+        $responseDiscriminator = new DialogueResponseDiscriminator();
+
+        $rootNode = new DialogueNode(
+            message: new ConstMessageProducer('What is your current role?'),
+            responseDiscriminator: $responseDiscriminator,
+        );
+
+        $marketingNode = new DialogueNode(
+            message: new ConstMessageProducer('Hello, marketer!'),
+            responseDiscriminator: $responseDiscriminator,
+        );
+
+        $rootNode->addResponse(new DialogueResponse(
+            when: new LlamaCppInputCondition(
+                Mockery::mock(LlamaCppClientInterface::class),
+                'marketing'
+            ),
+            followUp: $marketingNode,
+        ));
+
+        $rootNode->addResponse(new DialogueResponse(
+            when: new ExactInputCondition('marketing'),
+            followUp: $marketingNode,
+        ));
+
+        $invalidNode = new DialogueNode(
+            message: new ConstMessageProducer('nope :('),
+            responseDiscriminator: $responseDiscriminator,
+        );
+
+        $rootNode->addResponse(new DialogueResponse(
+            when: new ExactInputCondition('not_a_marketing'),
+            followUp: $invalidNode,
+        ));
+
+        $response = $rootNode->respondTo(new UserInput('marketing'));
+
+        self::assertSame($response, $marketingNode);
+    }
+}
diff --git a/src/DialogueResponse.php b/src/DialogueResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..8dfb37d4ee108db17deb2a229770eb9a6d4a38d3
--- /dev/null
+++ b/src/DialogueResponse.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+readonly class DialogueResponse implements DialogueResponseInterface
+{
+    public function __construct(
+        private DialogueNodeInterface $followUp,
+        private DialogueResponseConditionInterface $when,
+    ) {}
+
+    public function getCondition(): DialogueResponseConditionInterface
+    {
+        return $this->when;
+    }
+
+    public function getFollowUp(): DialogueNodeInterface
+    {
+        return $this->followUp;
+    }
+}
diff --git a/src/DialogueResponseCondition.php b/src/DialogueResponseCondition.php
new file mode 100644
index 0000000000000000000000000000000000000000..9196226c7d0edcd8a9aa9807ad6fee97a6a9ae2b
--- /dev/null
+++ b/src/DialogueResponseCondition.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+abstract readonly class DialogueResponseCondition implements DialogueResponseConditionInterface {}
diff --git a/src/DialogueResponseCondition/ExactInputCondition.php b/src/DialogueResponseCondition/ExactInputCondition.php
new file mode 100644
index 0000000000000000000000000000000000000000..20449344fc631b8271de78abd250cd46bb46e6e5
--- /dev/null
+++ b/src/DialogueResponseCondition/ExactInputCondition.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueResponseCondition;
+
+use Distantmagic\Resonance\DialogueInputInterface;
+use Distantmagic\Resonance\DialogueResponseCondition;
+
+readonly class ExactInputCondition extends DialogueResponseCondition
+{
+    public function __construct(
+        public string $content,
+    ) {}
+
+    public function getCost(): int
+    {
+        return 2;
+    }
+
+    public function isMetBy(DialogueInputInterface $dialogueInput): bool
+    {
+        return $dialogueInput->getContent() === $this->content;
+    }
+}
diff --git a/src/DialogueResponseCondition/LlamaCppInputCondition.php b/src/DialogueResponseCondition/LlamaCppInputCondition.php
new file mode 100644
index 0000000000000000000000000000000000000000..97d99fe5192e4e9a83d9501a0b9ff1fbc88334bc
--- /dev/null
+++ b/src/DialogueResponseCondition/LlamaCppInputCondition.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\DialogueResponseCondition;
+
+use Distantmagic\Resonance\DialogueInputInterface;
+use Distantmagic\Resonance\DialogueResponseCondition;
+use Distantmagic\Resonance\LlamaCppClientInterface;
+
+readonly class LlamaCppInputCondition extends DialogueResponseCondition
+{
+    public function __construct(
+        public LlamaCppClientInterface $llamaCppClient,
+        public string $content,
+    ) {}
+
+    public function getCost(): int
+    {
+        return 50;
+    }
+
+    public function isMetBy(DialogueInputInterface $dialogueInput): bool {}
+}
diff --git a/src/DialogueResponseConditionInterface.php b/src/DialogueResponseConditionInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..80e50ff9632854908d05f9677910eecc8439dd23
--- /dev/null
+++ b/src/DialogueResponseConditionInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueResponseConditionInterface
+{
+    public function getCost(): int;
+
+    public function isMetBy(DialogueInputInterface $dialogueInput): bool;
+}
diff --git a/src/DialogueResponseDiscriminator.php b/src/DialogueResponseDiscriminator.php
new file mode 100644
index 0000000000000000000000000000000000000000..628af587b2a8fe99180ef7a567ba3609e0343d81
--- /dev/null
+++ b/src/DialogueResponseDiscriminator.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+
+#[Singleton]
+readonly class DialogueResponseDiscriminator implements DialogueResponseDiscriminatorInterface
+{
+    /**
+     * @param iterable<DialogueResponseInterface> $responses
+     */
+    public function discriminate(
+        iterable $responses,
+        DialogueInputInterface $dialogueInput,
+    ): ?DialogueNodeInterface {
+        foreach (new DialogueResponseSortedIterator($responses) as $response) {
+            if ($response->getCondition()->isMetBy($dialogueInput)) {
+                return $response->getFollowUp();
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/src/DialogueResponseDiscriminatorInterface.php b/src/DialogueResponseDiscriminatorInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..60002459ba11e08cb6041fcd950ff855059e3567
--- /dev/null
+++ b/src/DialogueResponseDiscriminatorInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueResponseDiscriminatorInterface
+{
+    /**
+     * @param iterable<DialogueResponseInterface> $responses
+     */
+    public function discriminate(
+        iterable $responses,
+        DialogueInputInterface $dialogueInput,
+    ): ?DialogueNodeInterface;
+}
diff --git a/src/DialogueResponseInterface.php b/src/DialogueResponseInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..df2c67845f32a84753006ae2fb7f812b96f3d21f
--- /dev/null
+++ b/src/DialogueResponseInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueResponseInterface
+{
+    public function getCondition(): DialogueResponseConditionInterface;
+
+    public function getFollowUp(): DialogueNodeInterface;
+}
diff --git a/src/DialogueResponseResolution.php b/src/DialogueResponseResolution.php
new file mode 100644
index 0000000000000000000000000000000000000000..3dcfa0d8b2c93289805253453b5698170efce829
--- /dev/null
+++ b/src/DialogueResponseResolution.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+readonly class DialogueResponseResolution implements DialogueResponseResolutionInterface
+{
+    public function __construct(
+        private DialogueResponseResolutionStatus $status,
+    ) {}
+
+    public function getStatus(): DialogueResponseResolutionStatus
+    {
+        return $this->status;
+    }
+}
diff --git a/src/DialogueResponseResolutionInterface.php b/src/DialogueResponseResolutionInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ee9d1b4a82c6881f6126ff8e166fc7462430e53
--- /dev/null
+++ b/src/DialogueResponseResolutionInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface DialogueResponseResolutionInterface
+{
+    public function getStatus(): DialogueResponseResolutionStatus;
+}
diff --git a/src/DialogueResponseResolutionStatus.php b/src/DialogueResponseResolutionStatus.php
new file mode 100644
index 0000000000000000000000000000000000000000..136588fabb47a5d45e3dbbdb7e2c164bc5ca27ae
--- /dev/null
+++ b/src/DialogueResponseResolutionStatus.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+enum DialogueResponseResolutionStatus {}
diff --git a/src/DialogueResponseSortedIterator.php b/src/DialogueResponseSortedIterator.php
new file mode 100644
index 0000000000000000000000000000000000000000..de0aa5792e9efdc1b83173d4e25bb565ad130cc8
--- /dev/null
+++ b/src/DialogueResponseSortedIterator.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Ds\PriorityQueue;
+use Ds\Stack;
+use Generator;
+use IteratorAggregate;
+
+/**
+ * @template-implements IteratorAggregate<DialogueResponseInterface>
+ */
+readonly class DialogueResponseSortedIterator implements IteratorAggregate
+{
+    /**
+     * @param iterable<DialogueResponseInterface> $responses
+     */
+    public function __construct(
+        private iterable $responses,
+    ) {}
+
+    /**
+     * @return Generator<DialogueResponseInterface>
+     */
+    public function getIterator(): Generator
+    {
+        /**
+         * @var PriorityQueue<DialogueResponseInterface> $responsesPriorityQueue
+         */
+        $responsesPriorityQueue = new PriorityQueue();
+
+        foreach ($this->responses as $response) {
+            $responsesPriorityQueue->push(
+                $response,
+                $response->getCondition()->getCost(),
+            );
+        }
+
+        /**
+         * @var Stack<DialogueResponseInterface> $sortedResponses
+         */
+        $sortedResponses = new Stack();
+
+        foreach ($responsesPriorityQueue as $response) {
+            $sortedResponses->push($response);
+        }
+
+        foreach ($sortedResponses as $response) {
+            yield $response;
+        }
+    }
+}
diff --git a/src/DialogueResponseSortedIteratorTest.php b/src/DialogueResponseSortedIteratorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..82fa3b2617c1e1efc6022b42cc27d161f43318e6
--- /dev/null
+++ b/src/DialogueResponseSortedIteratorTest.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\DialogueMessageProducer\ConstMessageProducer;
+use Distantmagic\Resonance\DialogueResponseCondition\ExactInputCondition;
+use Distantmagic\Resonance\DialogueResponseCondition\LlamaCppInputCondition;
+use Mockery;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @internal
+ */
+#[CoversClass(DialogueResponseSortedIterator::class)]
+final class DialogueResponseSortedIteratorTest extends TestCase
+{
+    public function test_dialogue_responses_are_sorted_by_cost(): void
+    {
+        $responseDiscriminator = new DialogueResponseDiscriminator();
+
+        $marketingNode = new DialogueNode(
+            message: new ConstMessageProducer('Hello, marketer!'),
+            responseDiscriminator: $responseDiscriminator,
+        );
+
+        $response1 = new DialogueResponse(
+            when: new LlamaCppInputCondition(
+                Mockery::mock(LlamaCppClientInterface::class),
+                'test'
+            ),
+            followUp: $marketingNode,
+        );
+
+        $response2 = new DialogueResponse(
+            when: new ExactInputCondition('marketing'),
+            followUp: $marketingNode,
+        );
+
+        $responses = [
+            $response1,
+            $response2,
+        ];
+
+        $sortedResponses = iterator_to_array(new DialogueResponseSortedIterator($responses));
+
+        self::assertEquals([
+            $response2,
+            $response1,
+        ], $sortedResponses);
+    }
+}
diff --git a/src/EsbuildMetaBuilder.php b/src/EsbuildMetaBuilder.php
index 3c8720624dcb1cbd503d022b0abc68d1320f8ad5..91565d5f92de924e9610f61748695d3092e2fa4a 100644
--- a/src/EsbuildMetaBuilder.php
+++ b/src/EsbuildMetaBuilder.php
@@ -151,13 +151,13 @@ readonly class EsbuildMetaBuilder
             throw new RuntimeException('Esbuild meta manifest is not readable: '.$esbuildMetafile);
         }
 
-        $contents = Coroutine::readFile($esbuildMetafile);
+        $content = Coroutine::readFile($esbuildMetafile);
 
-        if (!is_string($contents)) {
+        if (!is_string($content)) {
             throw new RuntimeException('Unable to read esbuild manifest: '.$esbuildMetafile);
         }
 
-        return $contents;
+        return $content;
     }
 
     private function getEsbuildMetaDecoded(string $esbuildMetafile): object
diff --git a/src/HttpInterceptor.php b/src/HttpInterceptor.php
index d0aad021b042f22696f938ce4ecfe3945c356fd1..d0f4e5a62629dff9470f60bb4b040ce4eada9f51 100644
--- a/src/HttpInterceptor.php
+++ b/src/HttpInterceptor.php
@@ -14,8 +14,8 @@ use Stringable;
  */
 abstract readonly class HttpInterceptor implements HttpInterceptorInterface
 {
-    public function createStream(string|Stringable $contents): StreamInterface
+    public function createStream(string|Stringable $content): StreamInterface
     {
-        return new PsrStringStream($contents);
+        return new PsrStringStream($content);
     }
 }
diff --git a/src/HttpResponder.php b/src/HttpResponder.php
index 85b40c6d60c1addc2a536059161472c2c83507f3..a00045148596e646ac10224230bb9347bcaa9f57 100644
--- a/src/HttpResponder.php
+++ b/src/HttpResponder.php
@@ -9,8 +9,8 @@ use Stringable;
 
 abstract readonly class HttpResponder implements HttpResponderInterface
 {
-    public function createStream(string|Stringable $contents): StreamInterface
+    public function createStream(string|Stringable $content): StreamInterface
     {
-        return new PsrStringStream($contents);
+        return new PsrStringStream($content);
     }
 }
diff --git a/src/LlamaCppClient.php b/src/LlamaCppClient.php
index b991ee4ee092b22cc7f71bdb0bf7aa53cfa10ed2..831878abbe49f8fbb6f5f1eaffc3fa8f6402fef9 100644
--- a/src/LlamaCppClient.php
+++ b/src/LlamaCppClient.php
@@ -15,8 +15,8 @@ use Swoole\Coroutine;
 use Swoole\Coroutine\Channel;
 
 #[RequiresPhpExtension('curl')]
-#[Singleton]
-readonly class LlamaCppClient
+#[Singleton(provides: LlamaCppClientInterface::class)]
+readonly class LlamaCppClient implements LlamaCppClientInterface
 {
     public function __construct(
         private JsonSerializer $jsonSerializer,
diff --git a/src/LlamaCppClientInterface.php b/src/LlamaCppClientInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2ce4b469331a3f519cd9fbd05c7e00f88423c68
--- /dev/null
+++ b/src/LlamaCppClientInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Generator;
+
+interface LlamaCppClientInterface
+{
+    public function generateCompletion(LlamaCppCompletionRequest $request): LlamaCppCompletionIterator;
+
+    public function generateEmbedding(LlamaCppEmbeddingRequest $request): LlamaCppEmbedding;
+
+    /**
+     * @return Generator<LlamaCppInfill>
+     */
+    public function generateInfill(LlamaCppInfillRequest $request): Generator;
+
+    public function getHealth(): LlamaCppHealthStatus;
+}
diff --git a/src/PsrStringStream.php b/src/PsrStringStream.php
index 8c9dc8a344d2e09d7cadf224e18d4497f12f202b..94d5b0ddf51962fdf897cbad8509fe72776e6bd3 100644
--- a/src/PsrStringStream.php
+++ b/src/PsrStringStream.php
@@ -9,16 +9,16 @@ use Stringable;
 
 readonly class PsrStringStream implements StreamInterface
 {
-    private string $contents;
+    private string $content;
 
-    public function __construct(string|Stringable $contents)
+    public function __construct(string|Stringable $content)
     {
-        $this->contents = (string) $contents;
+        $this->content = (string) $content;
     }
 
     public function __toString(): string
     {
-        return $this->contents;
+        return $this->content;
     }
 
     public function close(): void {}
@@ -32,7 +32,7 @@ readonly class PsrStringStream implements StreamInterface
 
     public function getContents(): string
     {
-        return $this->contents;
+        return $this->content;
     }
 
     public function getMetadata($key = null)
@@ -42,7 +42,7 @@ readonly class PsrStringStream implements StreamInterface
 
     public function getSize(): ?int
     {
-        return strlen($this->contents);
+        return strlen($this->content);
     }
 
     public function isReadable(): bool
diff --git a/src/StaticPageInternalLinkNodeRenderer.php b/src/StaticPageInternalLinkNodeRenderer.php
index b0ba59a1e99d1f65e7491654bc437043f07bd746..9df9cf1c7d739b445cd7581cd8ef879991857eec 100644
--- a/src/StaticPageInternalLinkNodeRenderer.php
+++ b/src/StaticPageInternalLinkNodeRenderer.php
@@ -103,9 +103,9 @@ readonly class StaticPageInternalLinkNodeRenderer implements NodeRendererInterfa
     private function renderStaticPageBlockLink(StaticPage $staticPage): Stringable
     {
         /**
-         * @list<HtmlElement> $contents
+         * @list<HtmlElement> $content
          */
-        $contents = [
+        $content = [
             new HtmlElement(
                 'div',
                 [
@@ -135,7 +135,7 @@ readonly class StaticPageInternalLinkNodeRenderer implements NodeRendererInterfa
                 );
             }
 
-            $contents[] = new HtmlElement(
+            $content[] = new HtmlElement(
                 'ol',
                 [
                     'class' => 'document-links-group__link__tags',
@@ -150,7 +150,7 @@ readonly class StaticPageInternalLinkNodeRenderer implements NodeRendererInterfa
                 'class' => 'document-links-group__link',
                 'href' => $staticPage->getHref(),
             ],
-            $contents,
+            $content,
         );
     }
 
diff --git a/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php b/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
index 8c3603dec450c351275487fd87813a9b4508e457..bf45ed90a19bddccaa6b80eb79ba57b90d56ca11 100644
--- a/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
+++ b/src/WebSocketJsonRPCResponder/LlamaCppSubjectActionPromptResponder.php
@@ -6,7 +6,7 @@ namespace Distantmagic\Resonance\WebSocketJsonRPCResponder;
 
 use Distantmagic\Resonance\BackusNaurFormGrammar\SubjectActionGrammar;
 use Distantmagic\Resonance\JsonRPCRequest;
-use Distantmagic\Resonance\LlamaCppClient;
+use Distantmagic\Resonance\LlamaCppClientInterface;
 use Distantmagic\Resonance\LlamaCppCompletionIterator;
 use Distantmagic\Resonance\LlamaCppCompletionRequest;
 use Distantmagic\Resonance\LlmPrompt\SubjectActionPrompt;
@@ -59,7 +59,7 @@ abstract readonly class LlamaCppSubjectActionPromptResponder extends WebSocketJs
     abstract protected function toPromptTemplate(string $prompt): LlmPromptTemplate;
 
     public function __construct(
-        private LlamaCppClient $llamaCppClient,
+        private LlamaCppClientInterface $llamaCppClient,
         private LoggerInterface $logger,
         private ObservableTaskTable $observableTaskTable,
         private PromptSubjectResponderAggregate $promptSubjectResponderAggregate,