rawphp / laravel-capabilities-ai
Conversation / turn / proposal runtime for the Laravel Capabilities bus — pluggable LlmClient, progress store, bus-only tools
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- rawphp/laravel-capabilities: *
Requires (Dev)
- pestphp/pest: ^3.0
This package is auto-updated.
Last update: 2026-08-04 12:47:51 UTC
README
Status: 0.x pre-stable — not Packagist-published.
Install: package VCS or monorepo path.
Conversation / turn / proposal runtime for the Laravel Capabilities bus.
Monorepo path: packages/laravel-capabilities-ai/ in laravel-capabilities-monorepo.
Scope (this package)
| Is | Optional turn / proposal runtime: queue a turn, claim it, loop LLM → tools, stream progress (array/Redis); tool side effects only via CapabilityBus::invoke; host seams for conversation context and tool catalog; thin LlmClient (fake + Anthropic) for turns and host completions that must not embed domain rules |
| Is not | The capability bus / registry; chat channel bots (use messaging); product CLI; a general app-wide LLM SDK replacing laravel/ai; domain run(); generative UI or agent-native OS |
Requires rawphp/laravel-capabilities. Consumers install this package repo, not the monorepo.
Install (path package)
# monorepo root already path-wires this package
composer update rawphp/laravel-capabilities-ai
composer test:ai
Host app: require rawphp/laravel-capabilities-ai and register Rawphp\CapabilitiesAi\CapabilitiesAiServiceProvider (auto-discovery via extra.laravel.providers).
Config
Publish:
php artisan vendor:publish --tag=capabilities-ai-config php artisan vendor:publish --tag=capabilities-ai-migrations
Key defaults (config/capabilities-ai.php):
| Key | Default |
|---|---|
table_prefix |
capabilities_ai_ |
progress.driver |
array (or redis) |
llm.driver |
fake in tests / anthropic in prod |
claim_ttl |
120 |
max_tool_rounds |
8 |
routes.enabled |
false |
Progress events live in array/Redis — not MySQL product tables.
Host seams
Bind before running turns:
Rawphp\CapabilitiesAi\Contracts\ConversationContextProvider— messages for the modelRawphp\CapabilitiesAi\Contracts\ToolCatalog— tools the model may call (names = capability names)Rawphp\Capabilities\Contracts\CapabilityBus— already provided by core
use Rawphp\CapabilitiesAi\Contracts\LlmClient; use Rawphp\CapabilitiesAi\Support\FakeLlmClient; use Rawphp\CapabilitiesAi\Support\AnthropicLlmClient; // Testing default $app->bind(LlmClient::class, fn () => new FakeLlmClient); // Production $app->bind(LlmClient::class, fn () => new AnthropicLlmClient( apiKey: config('capabilities-ai.llm.anthropic.api_key'), model: config('capabilities-ai.llm.anthropic.model'), ));
Custom LlmClient: implement supportsToolRounds(). Prefer use LlmClientDefaults (returns false) and override to true only if the client accepts tool-result messages on the next complete() (OpenAI-style role=tool or Anthropic tool_result blocks). Lying opens a bus-then-crash path. (PHP interfaces still cannot ship method bodies on supported PHP; the trait is the fail-closed default for hosts.) Host upgrade callouts: user guide · CHANGELOG Breaking.
MVS product default: multi-round tools are off until a client opts in. AnthropicLlmClient stays false until real tool_result support ships; FakeLlmClient opts in for unit tests. Empty tool defs + refuse-before-bus is defense-in-depth for that default, not a second product surface.
Proposals (single accept/reject model): Accept returns typed AcceptOutcome for every known status (rejected/expired → refuse); HTTP maps outcomes + 404 when missing. Reject uses CAS + RuntimeException → 409 for non-pending. Host upgrade callouts: user guide · CHANGELOG Breaking.
- Accept: atomic CAS
pending → accepting, then bus invoke withidempotency_key=proposal:{ulid}(D-005). LiveIdempotencyReadinessprobe (fail closed) — not a constructor stamp. BranchisApprovalRequired()thenisHardRefuse()thenisRetryable(); approval/retry leave statusacceptingfor host re-drive. Hard non-retryable →failed+last_error. Success → atomicaccepting → accepted, clearlast_error. Returns typedAcceptOutcome(accepted|approval_required|retryable|failed|refuse). - Reject: atomic CAS
pending → rejectedonly; already-rejected is idempotent; accepting/accepted/failed/expired refuse (HTTP 409). - Recovery: stuck
acceptingis intentional (approval / retry / crash mid-accept). Package does not TTL-expire or reclaim; host re-drives accept under the same D-005 key (proposal:{ulid}). Hosts must wire coreIdempotencyStore(not an AI-package store) so the bus actually dedupes; readiness not ready → 503 without invoke. Conversation/tool bus invokes stay bare — only accept sets the proposal key.
Env: ANTHROPIC_API_KEY (never required in CI — tests use Http::fake / FakeLlmClient).
Flow
- Cheap create —
ConversationService::createUserMessageinserts message + queued turn, dispatchesRunTurnJob(no LLM). - Claim + run —
TurnClaimatomic update;TurnRunnerloops LLM → tools viaCapabilityBus::invokeonly. - Proposals —
ProposalService::accept/rejectas above (bus-only side effects on accept).
ProgressStore
use Rawphp\CapabilitiesAi\Support\ArrayProgressStore; use Rawphp\CapabilitiesAi\Support\RedisProgressStore; $store = new ArrayProgressStore; $store->append($turnUlid, ['kind' => 'status', 'data' => ['status' => 'running']]); $events = $store->since($turnUlid, $cursor);
Kinds: status | token | tool | error | terminal.
License
MIT
Non-chat / MVS host jobs
Hosts may resolve LlmClient without a Conversation (e.g. Macro Validation Suite jobs):
/** @var \Rawphp\CapabilitiesAi\Contracts\LlmClient $llm */ $llm = app(\Rawphp\CapabilitiesAi\Contracts\LlmClient::class); $result = $llm->complete([ ['role' => 'user', 'content' => 'Summarize this payload…'], ]);
The LlmClient interface has no conversation-only dependency. Testing default is FakeLlmClient (no network).