Agent-based AI workflows for Laravel with streaming support, built on Prism PHP.
- Streaming Agent Execution - Real-time token-by-token responses
- Tool/Function Calling - Agents can invoke tools during execution
- Agent Handoffs - Agents can delegate to other specialized agents with nested streaming
- Guardrails - Input/output validation before and after execution
- Tracing - Database persistence of execution traces with a web UI
- PHP 8.2+
- Laravel 11+
- Prism PHP 1.0+
composer require kaleidoscope/kaleidoscopePublish the configuration:
php artisan vendor:publish --tag=kaleidoscope-configRun the migrations:
php artisan migrateuse Kaleidoscope\Agent;
$agent = Agent::as('assistant')
->withInstructions('You are a helpful assistant that answers questions concisely.')
->using('anthropic', 'claude-sonnet-4-20250514')
->withMaxTokens(4096);use Kaleidoscope\Facades\Kaleidoscope;
$result = Kaleidoscope::run($agent, 'What is the capital of France?');
echo $result->getOutput(); // "Paris is the capital of France."foreach (Kaleidoscope::stream($agent, 'Tell me a story') as $event) {
match ($event->type) {
'text_delta' => echo $event->delta,
'tool_call' => handleToolCall($event),
'tool_result' => handleToolResult($event),
'stream_end' => break,
default => null,
};
}Route::get('/chat', function () {
$agent = Agent::as('assistant')
->withInstructions('You are helpful.')
->using('anthropic', 'claude-sonnet-4-20250514');
return Kaleidoscope::streamResponse($agent, request('message'));
});JavaScript client:
const eventSource = new EventSource('/chat?message=Hello');
eventSource.addEventListener('text_delta', (event) => {
const data = JSON.parse(event.data);
document.getElementById('output').textContent += data.delta;
});
eventSource.addEventListener('stream_end', () => {
eventSource.close();
});Add tools to give your agent capabilities:
use Prism\Prism\Tool;
$calculator = Tool::as('calculator')
->for('Perform mathematical calculations')
->withStringParameter('expression', 'The math expression to evaluate')
->using(function (string $expression): string {
return (string) eval("return {$expression};");
});
$agent = Agent::as('math-assistant')
->withInstructions('You help with math problems. Use the calculator tool.')
->using('anthropic', 'claude-sonnet-4-20250514')
->withTools([$calculator]);Create specialized agents that can hand off to each other:
$researcher = Agent::as('researcher')
->withInstructions('You research topics thoroughly.')
->withHandoffDescription('Delegate research tasks to this agent')
->using('anthropic', 'claude-sonnet-4-20250514');
$writer = Agent::as('writer')
->withInstructions('You write engaging content based on research.')
->withHandoffDescription('Delegate writing tasks to this agent')
->using('anthropic', 'claude-sonnet-4-20250514');
$coordinator = Agent::as('coordinator')
->withInstructions('You coordinate between the researcher and writer.')
->using('anthropic', 'claude-sonnet-4-20250514')
->withTools([$researcher, $writer]); // Agents as tools!
// The coordinator can now delegate to researcher or writer as needed
$result = Kaleidoscope::run($coordinator, 'Write a blog post about AI agents');When streaming, handoffs stream through the parent agent for a seamless experience:
foreach (Kaleidoscope::stream($coordinator, 'Write a blog post') as $event) {
if ($event->type === 'agent_handoff') {
echo "Handing off to: {$event->toAgent}\n";
}
// All text_delta events from sub-agents stream through
}Guardrails provide input and output validation for agents. They run before the agent processes input (input guardrails) and after the agent generates output (output guardrails), allowing you to enforce safety policies, content filtering, and business rules.
Implement the Guardrail interface:
use Kaleidoscope\Contracts\Guardrail;
use Kaleidoscope\AgentContext;
use Kaleidoscope\GuardrailResult;
class ContentFilter implements Guardrail
{
public function getName(): string
{
return 'content_filter';
}
public function validate(mixed $input, AgentContext $context): GuardrailResult
{
$blockedWords = ['inappropriate', 'harmful'];
foreach ($blockedWords as $word) {
if (str_contains(strtolower($input), $word)) {
return GuardrailResult::fail("Content contains blocked word: {$word}");
}
}
return GuardrailResult::pass();
}
}Use withInputGuardrails() and withOutputGuardrails() to attach guardrails:
$agent = Agent::as('safe-assistant')
->withInputGuardrails([new ContentFilter()])
->withOutputGuardrails([new ContentFilter()]);Guardrails return a GuardrailResult to indicate pass or fail:
// Passing validation
return GuardrailResult::pass();
// Passing with an optional message
return GuardrailResult::pass('Content is appropriate');
// Failing validation
return GuardrailResult::fail('Content violates policy');
// With metadata for logging/debugging
return GuardrailResult::fail('Rate limit exceeded', [
'current_rate' => 150,
'max_rate' => 100,
]);Guardrails receive the AgentContext, allowing validation based on user data or session state:
class RateLimitGuardrail implements Guardrail
{
public function getName(): string
{
return 'rate_limit';
}
public function validate(mixed $input, AgentContext $context): GuardrailResult
{
$userId = $context->get('user_id');
$requestCount = Cache::get("requests:{$userId}", 0);
if ($requestCount >= 100) {
return GuardrailResult::fail('Rate limit exceeded');
}
Cache::increment("requests:{$userId}");
return GuardrailResult::pass();
}
}
// Pass context when running the agent
$context = AgentContext::make('session')
->set('user_id', auth()->id());
$result = Kaleidoscope::run($agent, $input, $context);Multiple guardrails are checked in order. Execution stops at the first failure:
$agent = Agent::as('secure-assistant')
->withInputGuardrails([
new RateLimitGuardrail(),
new ContentFilter(),
new ProfanityFilter(),
])
->withOutputGuardrails([
new PIIRedactionGuardrail(),
new ToxicityFilter(),
]);When a guardrail fails, the result contains the error:
$result = Kaleidoscope::run($agent, 'blocked content');
if (! $result->isSuccess()) {
// Error message includes guardrail name and failure reason
echo $result->getError();
// "Guardrail 'content_filter' failed: Content contains blocked word: blocked"
}For more control, catch the GuardrailException:
use Kaleidoscope\Exceptions\GuardrailException;
try {
$result = Kaleidoscope::run($agent, $input);
} catch (GuardrailException $e) {
$guardrailName = $e->getGuardrailName();
$failureResult = $e->getResult();
Log::warning("Guardrail {$guardrailName} blocked request", [
'message' => $failureResult->getMessage(),
'metadata' => $failureResult->getMetadata(),
]);
}Guardrails work with streaming. Input guardrails run before streaming starts; if they fail, an error event is emitted:
foreach (Kaleidoscope::stream($agent, $input) as $event) {
if ($event->type === 'error') {
// Guardrail failure appears as an error event
echo "Blocked: {$event->error}";
break;
}
if ($event->type === 'text_delta') {
echo $event->delta;
}
}PII Detection:
class PIIGuardrail implements Guardrail
{
public function getName(): string
{
return 'pii_detection';
}
public function validate(mixed $input, AgentContext $context): GuardrailResult
{
// Check for SSN pattern
if (preg_match('/\b\d{3}-\d{2}-\d{4}\b/', $input)) {
return GuardrailResult::fail('Input contains SSN');
}
// Check for credit card pattern
if (preg_match('/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/', $input)) {
return GuardrailResult::fail('Input contains credit card number');
}
return GuardrailResult::pass();
}
}Length Validation:
class MaxLengthGuardrail implements Guardrail
{
public function __construct(private int $maxLength = 10000) {}
public function getName(): string
{
return 'max_length';
}
public function validate(mixed $input, AgentContext $context): GuardrailResult
{
if (strlen($input) > $this->maxLength) {
return GuardrailResult::fail(
"Input exceeds maximum length of {$this->maxLength} characters"
);
}
return GuardrailResult::pass();
}
}Topic Restriction:
class TopicGuardrail implements Guardrail
{
public function __construct(private array $blockedTopics) {}
public function getName(): string
{
return 'topic_restriction';
}
public function validate(mixed $input, AgentContext $context): GuardrailResult
{
$lowerInput = strtolower($input);
foreach ($this->blockedTopics as $topic) {
if (str_contains($lowerInput, strtolower($topic))) {
return GuardrailResult::fail("Discussion of '{$topic}' is not permitted");
}
}
return GuardrailResult::pass();
}
}
$agent = Agent::as('assistant')
->withInputGuardrails([
new TopicGuardrail(['politics', 'religion']),
]);Share data between agent executions:
use Kaleidoscope\AgentContext;
$context = AgentContext::make('user-session')
->set('user_id', 123)
->set('preferences', ['language' => 'en']);
$result = Kaleidoscope::run($agent, 'What are my settings?', $context);
// Child contexts inherit from parents
$childContext = $context->createChild(['additional' => 'data']);All agent executions are automatically traced when enabled. View traces at:
/kaleidoscope
Configure tracing in config/kaleidoscope.php:
'tracing' => [
'enabled' => env('KALEIDOSCOPE_TRACING_ENABLED', true),
'connection' => env('KALEIDOSCOPE_TRACING_CONNECTION', null),
'retention_days' => env('KALEIDOSCOPE_TRACING_RETENTION', 30),
],
'ui' => [
'enabled' => env('KALEIDOSCOPE_UI_ENABLED', true),
'path' => env('KALEIDOSCOPE_UI_PATH', 'kaleidoscope'),
'middleware' => ['web'],
],When streaming, you'll receive these event types:
| Event | Description |
|---|---|
stream_start |
Stream has started, includes agent/model info |
text_delta |
A chunk of generated text |
tool_call |
Agent is calling a tool |
tool_result |
Tool has returned a result |
agent_handoff |
Agent is handing off to another agent |
error |
An error occurred |
stream_end |
Stream has ended, includes usage stats |
// config/kaleidoscope.php
return [
'default_provider' => env('KALEIDOSCOPE_PROVIDER', 'anthropic'),
'default_model' => env('KALEIDOSCOPE_MODEL', 'claude-sonnet-4-20250514'),
'defaults' => [
'max_tokens' => 4096,
'temperature' => 1.0,
'max_steps' => 10, // Max tool call iterations
],
'tracing' => [
'enabled' => true,
'connection' => null, // Use default database
'retention_days' => 30,
],
'ui' => [
'enabled' => true,
'path' => 'kaleidoscope',
'middleware' => ['web'],
],
];composer testMIT