--- url: /tools/agent-skills.md --- # Agent Skills Iris supports [Agent Skills](https://agentskills.io) - a portable format for extending AI agents with specialized knowledge and workflows. ## Quick Start Install a skill using the CLI: ```bash npx skills add https://github.com/vercel-labs/skills --skill find-skills ``` Skills install to `.agents/skills/` and Iris picks them up automatically. Browse available skills at [skills.sh](https://skills.sh). ## How It Works 1. Iris sees available skill names and descriptions in its system prompt 2. When a task matches a skill's domain, Iris calls `activate_skill` to load full instructions 3. Skills can include references that Iris reads on-demand via shell > \[!NOTE] > Basic skill activation works without shell access. However, if a skill includes reference files, Iris needs [shell commands](/tools/shell-commands) enabled to read them. ## Pinning Skills to Threads By default, Iris activates skills on-demand — loading them when a task matches a skill's domain. **Pinned skills** take this further by injecting a skill's full content into the [system prompt](/core-concepts/system-prompts) for every message in a thread. This is useful when you want a skill's knowledge always active in a specific context. For example, pin a "Laravel development" skill to a thread where you're working through a project, so Iris always has those patterns available without needing to activate the skill each time. ### How to Pin Skills 1. Open the thread you want to configure 2. Click the **gear icon** in the chat header to open thread settings 3. Toggle the switch next to any skill to pin or unpin it 4. Click **Save** Pinned skills take effect on your next message — Iris loads their full content into the system prompt automatically. ### Context Consumption Pinned skills consume context window space on every message, since their full content is included in the system prompt. The thread settings modal shows a **context consumption notice** so you can see how much space your pinned skills use. > \[!TIP] > Pin only the skills you actively need for a thread. You can always unpin them later, and Iris can still activate unpinned skills on-demand when relevant. ### Missing Skills If a pinned skill has been removed from disk (deleted or renamed), the thread settings modal shows a **warning** next to it. You can unpin the missing skill to clean up the configuration. Missing skills are silently skipped during prompt building — they won't cause errors. ### Per-Thread Configuration Pinned skills are configured per [thread](/core-concepts/threads). Different threads can have different pinned skills, letting you tailor Iris's expertise to each conversation context. Skill settings are stored in the thread's `settings` column. ## Configuration Skills are enabled by default: ```bash # .env IRIS_SKILLS_ENABLED=true IRIS_SKILLS_DIR=/path/to/project/.agents/skills ``` ### Disabling Skills ```bash # .env IRIS_SKILLS_ENABLED=false ``` Or disable just the activation tool: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\Skill\ActivateSkillTool::class, ], ]; ``` ## Resources * [Agent Skills Specification](https://agentskills.io) - The open format spec * [skills.sh](https://skills.sh) - Browse and discover skills * `npx skills` - CLI for installing and managing skills --- --- url: /architecture/background-jobs.md --- # Background Jobs Iris processes memory extraction, conversation summarization, and memory consolidation in the background so your chat experience stays fast and responsive. Here's how the pieces fit together. ## Why Background Jobs? When you send a message, you want a quick response. But extracting memories and generating summaries involves LLM calls that can take several seconds. By running these as background jobs: * **Chat stays fast** - Responses stream immediately * **Processing is reliable** - Jobs retry on failure * **Load is distributed** - Jobs run when capacity is available ## Running Horizon Iris uses [Laravel Horizon](https://laravel.com/docs/horizon) for queue management: ```bash # For development (included in composer dev) php artisan horizon ``` Horizon provides: * Dashboard at `/horizon` for monitoring * Automatic process management * Job metrics and failure tracking > \[!IMPORTANT] > Without Horizon running, chat messages won't process, memories won't be extracted, and conversations won't be summarized. ## Chat Processing Chat messages are processed asynchronously within the active [thread](/core-concepts/threads), enabling reliable delivery even when connections are unstable. ### How It Works 1. When you send a message, a job is queued for the active thread 2. The job builds thread-scoped context and processes your request, executing tools as needed 3. Response events broadcast to clients viewing that thread via WebSockets 4. If your connection drops, events are stored for replay ### Reliability Features | Feature | Benefit | |---------|---------| | Automatic retries | Failed requests retry with exponential backoff | | Event storage | Reconnecting clients catch up automatically | | Graceful stops | Users can stop generation mid-stream | ## Automatic Jobs These jobs dispatch automatically after Iris processes a response, based on configurable thresholds. ### ExtractMemories Pulls meaningful information from recent conversations and stores it as searchable memories with embeddings. | Setting | Default | Description | |---------|---------|-------------| | Trigger | Every 10 messages | Configurable via `iris.extraction.threshold` | | Max per run | 6 memories | Prevents over-extraction from a single batch | | Timeout | 120 seconds | Maximum LLM call duration | The job builds context from up to 50 recent messages, uses an LLM to identify what's worth remembering, then creates memory records with vector embeddings for semantic search. See [Memory Extraction](/core-concepts/memory-extraction) for details on what gets extracted. ### SummarizeConversation Creates narrative summaries of older conversations within a [thread](/core-concepts/threads), capturing emotional context and relationship dynamics. Each thread maintains its own independent summary chain. | Setting | Default | Description | |---------|---------|-------------| | Trigger | 40+ unsummarized messages in thread | After keeping 35 recent messages as buffer | | Timeout | 120 seconds | Maximum LLM call duration | Summaries chain together via `previous_summary_id` within the thread, maintaining conversational continuity. Each summary includes emotional markers, resolved/unresolved threads, and relationship dynamics. See [Summarization](/core-concepts/summarization) for details on what summaries capture. ### GenerateThreadBrief Generates a [thread brief](/core-concepts/thread-briefs) — a 3-6 sentence digest of what a thread is about, including topic, key decisions, current status, and emotional context. Briefs power cross-thread awareness in chat and provide context for [proactive message](/core-concepts/proactive-messages) decisions. | Setting | Default | Description | |---------|---------|-------------| | Trigger | Every `briefs.frequency` assistant responses (default: 2) | After minimum `briefs.threshold` responses | | Model | `claude-haiku-4-5` | Fast, inexpensive model for brief generation | | Timeout | 15 seconds | Maximum LLM call duration | The job loads the last ~10 messages, the existing brief (for continuity), and the latest conversation summary (if one exists) for structured context. Output is sanitized and stored on the thread model. The job implements `ShouldBeUnique` with `"brief-{threadId}"` to prevent concurrent generation for the same thread. ### GenerateThreadName Automatically generates a short, descriptive name for new threads based on conversation content. | Setting | Default | Description | |---------|---------|-------------| | Trigger | After `threads.naming_threshold` messages (default: 4) | Only for threads not manually renamed | | Model | `claude-haiku-4-5` | Fast, inexpensive model for naming | | Timeout | 15 seconds | Maximum LLM call duration | The job validates LLM output (rejects multi-sentence prose, refusals, or overly long names) and broadcasts the new name to all connected clients via WebSocket. ## Scheduled Jobs ### Memory Consolidation Consolidation merges semantically similar memories into denser, more useful representations. It runs on two schedules: * **Daily (3 AM)** - Processes memories from the last 3 days * **Weekly (Sunday 4 AM)** - Full sweep of all memories Consolidation uses a two-phase job architecture: 1. `ConsolidateUserMemories` - Builds clusters of similar memories 2. `ConsolidateMemoryCluster` - Processes each cluster with LLM review See [Memory Consolidation](/core-concepts/memory-consolidation) for the full details on how it works, generation tracking, and command options. ## Agent Daemon Delegated sub-agent tasks are processed by the `iris:agent` daemon — a separate long-running process that operates independently of Horizon. ### Why a Daemon? Sub-agent tasks can run for several minutes and involve streaming LLM responses with real-time tool call broadcasting. This doesn't fit well into a traditional queue worker model. The daemon provides: * **Sequential processing** — one task at a time, preventing resource exhaustion * **Inline rate limit handling** — sleeps and retries when API limits are hit, rather than releasing back to a queue * **Graceful shutdown** — responds to `SIGINT`/`SIGTERM` signals, finishing the current task before stopping * **Exponential backoff** — when idle, polling frequency decreases to reduce overhead ### Running the Daemon ```bash # Included automatically in composer dev php artisan iris:agent ``` The daemon polls for pending tasks, processes them using `AgentTaskRunner`, and broadcasts progress events via WebSockets. Task results are delivered as proactive messages in the originating [thread](/core-concepts/threads). > \[!IMPORTANT] > The agent daemon is not managed by Horizon and won't appear in the Horizon dashboard. Monitor it through your process manager or terminal output. See [Task Delegation](/tools/task-delegation) for feature details and configuration. ## Monitoring Jobs ### Horizon Dashboard Access the Horizon dashboard at `/horizon` to monitor: * Job throughput and processing times * Failed job details and stack traces * Queue lengths and wait times * Memory and process health ### Queue Commands ```bash # Check failed jobs php artisan queue:failed # Retry failed jobs php artisan queue:retry all ``` ### Logging Jobs log their activity to Laravel's default log channel. Check `storage/logs/laravel.log` for: * Job start/completion timestamps * Extraction and summarization results * Error details for failed jobs ## Failure Handling ### Automatic Retries Jobs are configured with retry policies: | Job | Retries | Backoff | |-----|---------|---------| | ProcessChatRequest | 3 | Exponential | | ExtractMemories | 3 | Exponential | | SummarizeConversation | 3 | Exponential | | ConsolidateMemoryCluster | Time-based (2 hours) | Rate-limit aware | ### Rate Limit Handling Consolidation jobs handle API rate limits specially: * Jobs release back to queue when rate limited * Automatic retry when the limit resets * Uses Prism's `resetsAt` timing for precise retry scheduling ### Manual Intervention If jobs are persistently failing: ```bash # View failed jobs php artisan queue:failed # Retry specific job php artisan queue:retry # Clear failed jobs php artisan queue:flush ``` ## Configuration Key settings in `config/iris.php`: ```php 'extraction' => [ 'threshold' => 10, // Messages between extractions 'max_memories' => 6, // Max memories per run 'timeout' => 120, // API timeout in seconds 'model' => 'claude-sonnet-4-5', ], 'summarization' => [ 'threshold' => 40, // Unsummarized messages to trigger 'buffer' => 35, // Buffer before summarizing old messages 'timeout' => 120, // API timeout in seconds 'model' => 'claude-sonnet-4-5', ], 'consolidation' => [ 'jobs_per_minute' => 10, // Rate limit for LLM calls // ... other settings ], ``` --- --- url: /tools/built-in-tools.md --- # Built-in Tools Iris comes with tools for truths, memory management, calendar operations, follow-up scheduling, image generation, and web access. This reference documents each tool's parameters and typical usage. ## Truth Tools [Truths](/core-concepts/truths) are stable, core facts about the user - more permanent than memories. These tools let Iris manage Truths during conversation. ### store\_truth Store a core truth about the user - fundamental facts that define who they are. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `truthContent` | string | Yes | Complete, self-contained statement about the user | | `category` | string | No | personal, professional, hobbies, health, relationships, preferences, goals | | `isPinned` | boolean | No | Pin to always include regardless of conversation topic (default: false) | **Usage example:** > "My name is TJ and I'm a software engineer in NYC - that's important to remember" Iris calls: ``` store_truth( truthContent: "TJ is a software engineer based in New York City", category: "professional", isPinned: false ) ``` **Returns:** Confirmation with truth ID and pin status. > \[!TIP] > Use Truths for stable identity facts (name, career, location, core relationships). Use memories for contextual or time-bound information. ### search\_truths Search existing truths semantically. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `searchQuery` | string | Yes | Question or topic to search for | | `limit` | number | No | Max results 1-10 (default: 5) | **Usage example:** > "What core facts do you know about me?" ``` search_truths(searchQuery: "identity career location relationships") ``` **Returns:** List of matching truths with content, source, and pin status. ### update\_truth Modify an existing truth when core facts change. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `truthId` | number | Yes | ID of truth to update | | `updatedContent` | string | Yes | New content (replaces old) | **Usage example:** > "I got promoted to tech lead - update my job title" ``` update_truth( truthId: 5, updatedContent: "TJ is a tech lead at a fintech company in New York City" ) ``` **Returns:** Confirmation of the update. ### delete\_truth Remove a truth permanently. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `truthId` | number | Yes | ID of truth to delete | **Usage example:** > "Forget that fact about my old job" **Returns:** Confirmation of deletion. > \[!NOTE] > When you delete a truth, similar memories won't be promoted to replace it - the system respects your explicit deletion. ## Memory Tools These tools let Iris manage persistent memories during conversation. They complement [automatic extraction](/core-concepts/memory-extraction) - use them for contextual information, events, and situational details. ### store\_memory Store a new memory about the user. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `memoryContent` | string | Yes | Complete, self-contained fact with context | | `memoryType` | string | No | fact, preference, goal, event, skill, relationship, habit, context | | `tags` | array | No | Organization tags | | `category` | string | No | personal, professional, hobbies, health, etc. | **Usage example:** > "Remember that I'm allergic to shellfish" Iris calls: ``` store_memory( memoryContent: "User is allergic to shellfish", memoryType: "fact", category: "health" ) ``` **Returns:** Confirmation message with memory ID. > \[!TIP] > The `memoryContent` should be self-contained -it should make sense without the conversation context. "Allergic to shellfish" is better than "Has the allergy we discussed." ### search\_memory Search stored memories semantically. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `searchQuery` | string | Yes | Question or topic to search for | | `memoryType` | string | No | Filter by type | | `tags` | array | No | Filter by tags | | `category` | string | No | Filter by category | | `limit` | number | No | Max results 1-20 (default: 5) | **Usage example:** > "What do you know about my dietary restrictions?" Iris calls: ``` search_memory( searchQuery: "dietary restrictions food allergies", memoryType: "fact", limit: 5 ) ``` **Returns:** List of matching memories with content and metadata. ### update\_memory Modify an existing memory. Useful when information changes or was recorded incorrectly. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `memoryId` | number | Yes | ID of memory to update | | `updatedContent` | string | Yes | New content (replaces old) | **Usage example:** > "Actually, I moved to Chicago last month. Update my location." Iris first searches for the location memory, then calls: ``` update_memory( memoryId: 42, updatedContent: "User lives in Chicago (moved from Seattle in December 2025)" ) ``` **Returns:** Confirmation of the update. > \[!NOTE] > Updating regenerates the memory's embedding, so semantic search will find it with the new content. ### delete\_memory Remove a memory permanently. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `memoryId` | number | Yes | ID of memory to delete | **Usage example:** > "Forget that I like pineapple on pizza -I changed my mind." **Returns:** Confirmation of deletion. ## Calendar Tools These tools interact with Google Calendar. They require a [connected Google account](/integrations/google-calendar). ### list\_calendar\_events Fetch upcoming events. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `days` | number | No | Days ahead, 1-30 (default: 7) | | `calendarId` | string | No | Specific calendar to filter by | **Usage example:** > "What's on my calendar next week?" ``` list_calendar_events(days: 7) ``` **Returns:** List of events with title, time, location, and calendar name. **Error responses:** * "Google Calendar is not connected" - User needs to connect in Settings * "No events found" - Calendar is empty for the requested period ### create\_calendar\_event Create a new calendar event. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `title` | string | Yes | Event title | | `startDateTime` | string | Yes | ISO 8601 format with timezone | | `endDateTime` | string | Yes | ISO 8601 format with timezone | | `description` | string | No | Event notes | | `location` | string | No | Address or meeting link | | `addMeetLink` | boolean | No | Add Google Meet link | **Usage example:** > "Schedule a team sync tomorrow at 10am for 30 minutes" ``` create_calendar_event( title: "Team Sync", startDateTime: "2026-01-29T10:00:00-05:00", endDateTime: "2026-01-29T10:30:00-05:00", addMeetLink: true ) ``` **Returns:** Event ID and confirmation with Meet link if requested. > \[!IMPORTANT] > DateTime must be ISO 8601 format with timezone. Iris handles the conversion from natural language. ### update\_calendar\_event Modify an existing event. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `eventId` | string | Yes | Event to update | | `title` | string | No | New title | | `startDateTime` | string | No | New start time | | `endDateTime` | string | No | New end time | | `description` | string | No | New description | | `location` | string | No | New location | **Usage example:** > "Move my dentist appointment to 3pm" Iris finds the event, then calls: ``` update_calendar_event( eventId: "abc123", startDateTime: "2026-01-29T15:00:00-05:00", endDateTime: "2026-01-29T16:00:00-05:00" ) ``` ### delete\_calendar\_event Remove an event from the calendar. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `eventId` | string | Yes | Event to delete | **Returns:** Confirmation of deletion. ## Follow-up Tools These tools let Iris schedule and manage future check-ins during conversation. When Iris says "I'll check in on that later," these tools make it real. ### schedule\_followup Schedule a future check-in with the user. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `context` | string | Yes | What to follow up about - be specific for future context | | `minutes_from_now` | number | Yes | How many minutes from now to follow up | **Usage example:** > "I have a job interview in an hour, wish me luck!" Iris calls: ``` schedule_followup( context: "Check in after job interview to see how it went", minutes_from_now: 90 ) ``` **Returns:** Confirmation with follow-up ID and scheduled time. > \[!TIP] > Follow-ups are processed by the [proactive messages system](/core-concepts/proactive-messages). The user must have proactive messages enabled in Settings for follow-ups to trigger. ### list\_followups List scheduled follow-ups. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `status` | string | No | pending (default), completed, or all | | `limit` | number | No | Max results 1-50 (default: 10) | **Usage example:** > "What follow-ups do you have scheduled?" ``` list_followups(status: "pending") ``` **Returns:** List of follow-ups with ID, context, scheduled time, and status. ### update\_followup Update the context or reschedule a pending follow-up. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `followup_id` | number | Yes | ID of follow-up to update | | `context` | string | No | New context (replaces old) | | `minutes_from_now` | number | No | Reschedule to this many minutes from now | At least one of `context` or `minutes_from_now` must be provided. **Usage example:** > "Actually, push that check-in back another hour" ``` update_followup( followup_id: 12, minutes_from_now: 120 ) ``` **Returns:** Confirmation with updated details. **Error responses:** * "not found" - Follow-up doesn't exist or belongs to another user * "already completed" - Can't update a completed follow-up ### cancel\_followup Cancel a pending follow-up. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `followup_id` | number | Yes | ID of follow-up to cancel | | `reason` | string | No | Reason for cancellation | **Usage example:** > "Never mind about that check-in, it's resolved" ``` cancel_followup( followup_id: 12, reason: "Issue resolved during conversation" ) ``` **Returns:** Confirmation of cancellation. ## Image Generation ### generate\_image Generate an image from a text description using OpenAI's GPT Image model. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `description` | string | Yes | Detailed image description | | `size` | string | No | 1024x1024, 1536x1024, or 1024x1536 (default: 1024x1024) | **Usage example:** > "Create an image of a cozy coffee shop" ``` generate_image( description: "A cozy coffee shop with warm lighting, wooden tables, plants on windowsills, and a cat sleeping on a cushioned chair", size: "1536x1024" ) ``` **Returns:** The generated image displayed inline in chat, plus storage path. **Error responses:** * "OpenAI API key not configured" - Add `OPENAI_API_KEY` to `.env` * "Content policy violation" - The prompt was rejected by OpenAI's safety filters See [Image Generation](/integrations/image-generation) for more details on prompt strategies and limitations. ## Provider Tools Provider tools are built into Anthropic's API and run on their infrastructure: ```php // config/iris.php 'provider_tools' => [ ['type' => 'web_fetch_20250910', 'name' => 'web_fetch'], ['type' => 'web_search_20250305', 'name' => 'web_search'], ], ``` ### web\_search Search the web for current information. Anthropic handles the search on their servers. **Usage example:** > "What's the latest news about Laravel?" Iris calls web\_search internally, receives results, and synthesizes a response. **Best for:** * Recent news and events * Current documentation * Fact-checking information * Researching topics ### web\_fetch Retrieve content from a specific URL. **Usage example:** > "Read this article: https://example.com/blog/post" **Best for:** * Reading specific articles or documentation * Fetching content from URLs the user provides * Accessing public APIs **Limitations:** * Some sites block automated access * Very large pages may be truncated * JavaScript-rendered content may not be accessible > \[!TIP] > To disable provider tools entirely, set `provider_tools` to an empty array in `config/iris-custom.php`. ## Shell Commands > \[!WARNING] > Shell command execution is **disabled by default**. Set `IRIS_SHELL_ENABLED=true` in your `.env` to enable. Execute shell commands for file operations, system queries, and CLI tasks. See the [Shell Commands](/tools/shell-commands) guide for full details. ### run\_shell\_command Execute a shell command on the host operating system. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `command` | string | Yes | The shell command to execute | | `workingDirectory` | string | No | Directory for command execution | | `timeout` | number | No | Timeout in seconds (default: 30, max: 300) | **Usage example:** > "Show me the git log for my project at ~/code/myapp" Iris calls: ``` run_shell_command( command: "git log --oneline -10", workingDirectory: "/Users/you/code/myapp" ) ``` **Returns:** Command output with exit code, stdout, and stderr. > \[!NOTE] > Privilege escalation commands (`sudo`, `su`) and dangerous patterns are blocked. Commands run in a clean environment without access to sensitive variables. ## Filesystem (Beta) > \[!WARNING] > Filesystem tools are **disabled by default**. Set `IRIS_FILESYSTEM_ENABLED=true` in your `.env` to enable. Read, write, search, and navigate files within a scoped workspace directory. This is an initial implementation - the API may change or be removed in future releases. See the [Filesystem Tools](/tools/filesystem-tools) guide for full setup, parameter tables, the containment model, and denylist configuration. ### read\_file Read the contents of a file inside the workspace. Implemented by `App\Tools\Filesystem\ReadFileTool`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to read | | `offset` | number | No | Line number to start reading from | | `limit` | number | No | Maximum number of lines to return | **Usage example:** > "Show me the contents of src/config.php" Iris calls: ``` read_file(path: "src/config.php") ``` **Returns:** File contents as text, truncated at 1 MB unless `offset` or `limit` is supplied. ### write\_file Write content to a file inside the workspace, creating it or overwriting an existing one. Implemented by `App\Tools\Filesystem\WriteFileTool`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to write | | `content` | string | Yes | Full content to write to the file | **Usage example:** > "Create a new file called notes.md with a placeholder header" Iris calls: ``` write_file( path: "notes.md", content: "# Notes\n" ) ``` **Returns:** Confirmation with relative path and bytes written. > \[!NOTE] > Overwriting an existing file requires that file to have been read via `read_file` in the same session. New (non-existent) files can be written without a prior read. ### edit\_file Replace a specific string inside an existing file. Implemented by `App\Tools\Filesystem\EditFileTool`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to edit | | `old_string` | string | Yes | Exact string to find and replace | | `new_string` | string | Yes | Replacement string | | `replace_all` | boolean | No | Replace every occurrence instead of requiring exactly one (default: false) | **Usage example:** > "Change the function name from getUser to findUser in app/Services/UserService.php" Iris calls: ``` edit_file( path: "app/Services/UserService.php", old_string: "function getUser(", new_string: "function findUser(" ) ``` **Returns:** Confirmation with the number of replacements made. > \[!NOTE] > `edit_file` requires the target file to have been read via `read_file` in the same session. If `old_string` matches more than once and `replace_all` is false, the edit is rejected. ### grep Search file contents for a pattern across the workspace. Implemented by `App\Tools\Filesystem\GrepTool`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pattern` | string | Yes | Regular expression to search for | | `path` | string | No | Workspace-relative subdirectory to limit the search | | `glob` | string | No | Glob pattern to filter files (e.g. `*.php`) | | `output_mode` | string | No | `files_with_matches` (default), `content`, or `count` | **Usage example:** > "Find all files that reference the UserService class" Iris calls: ``` grep( pattern: "UserService", glob: "*.php" ) ``` **Returns:** Matching file paths, `path:line: content` lines when `output_mode` is `content`, or match counts per file when `output_mode` is `count`. ### glob List files matching a glob pattern inside the workspace. Implemented by `App\Tools\Filesystem\GlobTool`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pattern` | string | Yes | Glob pattern relative to the workspace root (e.g. `src/**/*.ts`) | **Usage example:** > "List all PHP test files in the tests directory" Iris calls: ``` glob(pattern: "tests/**/*.php") ``` **Returns:** Workspace-relative paths of all matching files, sorted by modification time. ## Task Delegation (Beta) > \[!WARNING] > Task delegation is **disabled by default**. Requires both `IRIS_SUBAGENT_ENABLED=true` and `IRIS_SHELL_ENABLED=true` in your `.env`. Delegate complex, multi-step tasks to an autonomous sub-agent. This is an initial implementation - the API may change or be removed in future releases. See the [Task Delegation](/tools/task-delegation) guide for full details. ### delegate\_task Delegate a complex task to a sub-agent for autonomous execution. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `description` | string | Yes | Clear description of what needs to be done | | `successCriteria` | array | No | Verifiable conditions that define completion | | `contextFiles` | array | No | File paths relevant to the task | | `workingDirectory` | string | No | Directory for task execution | | `timeout` | number | No | Timeout in seconds (default: 300, max: 600) | **Usage example:** > "Set up a new Laravel project with Breeze authentication" Iris calls: ``` delegate_task( description: "Create Laravel project with Breeze auth scaffolding", successCriteria: [ "Laravel project created", "Breeze installed and configured", "Migrations run successfully" ], workingDirectory: "/home/user/projects" ) ``` **Returns:** Task completion status with output, duration, and modified files. > \[!NOTE] > Each user can only have one active delegated task at a time. The sub-agent inherits all shell security settings. --- --- url: /core-concepts/cache-breakpoints.md --- # Cache Breakpoints Cache breakpoints let you tell Anthropic's API to cache stable parts of your system prompt, so repeated requests reuse the cached prefix instead of re-processing it. This cuts input token costs by up to 90% and reduces latency on every message. ## How Prompt Caching Works Anthropic uses **prefix caching**: a cache breakpoint marks a point in the system prompt and tells the API "cache everything from the very start up to here." On subsequent requests, if the content before the breakpoint is byte-identical, the API serves it from cache at 10% of the normal input token cost. The key constraint is that Anthropic allows a **maximum of 4 cache breakpoints** per request. If you try to set more than 4, the API returns a 400 error. This is why Iris centralizes cache management in the config rather than letting individual prompt classes set their own caching — it's too easy to exceed the limit when prompts independently decide to cache themselves. A few things to keep in mind about how caching works: * Caching is **prefix-based**. A breakpoint at position N caches everything from position 0 through N. If anything before the breakpoint changes, the cache misses entirely. * Cache hits are **10% of the normal input token cost**. For a 50K token system prompt, that's the difference between paying for 50K tokens and paying for 5K tokens on every message. * Only content **before or at** a breakpoint is cached. Content after the last breakpoint is always processed fresh. ## Cache Groups Iris organizes system prompts into **cache groups** in the `prompts` config. Each group bundles related prompts together and optionally marks them for caching with a single breakpoint. Here's the default configuration from `config/iris.php`: ```php 'prompts' => [ // Group 1: Static content — one breakpoint at the end [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ IrisStaticPrompt::class, AutonomousExecutionPrompt::class, SkillsPrompt::class, ], ], // Group 2: Pinned content — one breakpoint at the end [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ PinnedSkillsPrompt::class, PinnedPromptsPrompt::class, ], ], // Standalone prompts — no caching (dynamic per-request content) MemoryPrompt::class, SummaryPrompt::class, CalendarPrompt::class, CurrentTimePrompt::class, ], ``` There are two types of entries in the `prompts` array: ### Cache Groups (Arrays) An array entry with `cache` and `prompts` keys defines a cache group. The group produces **one cache breakpoint**, applied to the last non-empty message in the group. | Key | Type | Required | Description | |-----|------|----------|-------------| | `cache.type` | `string` | Yes | Cache type. Use `'ephemeral'` (the only currently supported type). | | `cache.ttl` | `string` | No | Cache duration. Either `'5m'` (default) or `'1h'`. See [Cache TTL Options](#cache-ttl-options). | | `prompts` | `array` | Yes | Ordered list of prompt class names to render in this group. | ### Standalone Entries (Strings) A plain class name string is a standalone prompt with no caching. Use these for dynamic, per-request content like memories, summaries, and calendar events that change between requests. ### Where the Breakpoint Lands Within a cache group, the breakpoint is applied to the **last non-empty message** produced by the group's prompts. If a prompt's `content()` returns an empty string, it doesn't generate a message and doesn't affect breakpoint placement. This means: * If a group has 3 prompts and all produce content, the breakpoint lands on the 3rd prompt's message. * If the last prompt in a group returns empty content, the breakpoint moves to the previous non-empty prompt's message. * If all prompts in a group return empty content, no breakpoint is consumed. The group effectively becomes invisible. ## How Groups Map to Breakpoints Here's how the default configuration maps to actual cache breakpoints in the API request: ``` ┌─ Group 1 (cached, 1h TTL) ──────────────────┐ │ IrisStaticPrompt │ │ AutonomousExecutionPrompt │ │ SkillsPrompt ← BP #1 │ └──────────────────────────────────────────────┘ ┌─ Group 2 (cached, ephemeral) ────────────────┐ │ PinnedSkillsPrompt │ │ PinnedPromptsPrompt ← BP #2 │ └──────────────────────────────────────────────┘ MemoryPrompt (dynamic) SummaryPrompt (dynamic) CalendarPrompt (dynamic) CurrentTimePrompt (dynamic) ``` **Breakpoint #1** caches everything from the start of the system prompt through the end of `SkillsPrompt`. This covers the core identity, autonomous execution behavior, and skill definitions — content that rarely changes between requests. **Breakpoint #2** caches everything from the start through `PinnedPromptsPrompt`. Because caching is prefix-based, this breakpoint extends the cached region to include the pinned skills and prompts. If no skills are pinned (both prompts return empty), this group doesn't consume a breakpoint at all. The remaining prompts — memories, summaries, calendar, and current time — are dynamic. They change with every request, so caching them would just waste write costs on content that never gets a cache hit. That leaves **2 breakpoints unused** out of the 4 available, giving you room to add your own cached groups for custom content. ## Cache TTL Options The `ttl` option controls how long cached content lives and what it costs to write: | TTL | Write Cost | Best For | |-----|-----------|----------| | `'5m'` (default) | 1.25x normal input cost | Content that might change within an hour (pinned skills, semi-dynamic content) | | `'1h'` | 2x normal input cost | Truly static content that rarely changes (core identity, behavior rules) | When you omit `ttl`, it defaults to `'5m'`. The default Iris config uses `'1h'` for Group 1 (static identity and skills) because that content almost never changes during a session, and the longer TTL means fewer cache misses over an hour of conversation. Group 2 uses the default `'5m'` because pinned skills can change when a user pins or unpins a skill. > \[!TIP] > The 1h TTL costs 2x to write but only 0.1x to read. If content stays stable for an hour, you break even after just one cache hit and save on every subsequent request. For truly static content, `'1h'` almost always pays for itself. ## Customizing Cache Groups All prompt customization goes in `config/iris-custom.php`. The `prompts` key **replaces** the default list entirely, so you'll need to include the core prompts you want to keep. ### Adding a Custom Prompt to an Existing Group If your custom prompt is static content (team info, project context, style guides), add it to an existing cached group so it shares that group's breakpoint: ```php // config/iris-custom.php return [ 'prompts' => [ [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, App\Extensions\Prompts\TeamStyleGuidePrompt::class, // Added to Group 1 ], ], [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, ], ], App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` This doesn't use an additional breakpoint — the `TeamStyleGuidePrompt` content is included in the cached region alongside the other static prompts. ### Creating a New Cached Group If you have custom content that changes at a different cadence than the core prompts, give it its own group: ```php // config/iris-custom.php return [ 'prompts' => [ [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, ], ], [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, ], ], // New group: project context that updates daily [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Extensions\Prompts\ActiveProjectsPrompt::class, App\Extensions\Prompts\SprintGoalsPrompt::class, ], ], App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` This uses 3 of your 4 available breakpoints. The project context changes less frequently than memories or calendar events, so caching it saves tokens across a conversation session. ### Removing Caching Entirely If you're not using Anthropic (or just don't want to think about caching), list all prompts as standalone strings: ```php // config/iris-custom.php return [ 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` No groups, no breakpoints, no caching. Everything works the same — you just don't get the cost savings from Anthropic's prompt cache. ### Moving Prompts Between Groups You can reorganize which prompts share a breakpoint. For example, if you want pinned skills cached alongside the static content: ```php // config/iris-custom.php return [ 'prompts' => [ // Single group for all cacheable content [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, ], ], App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` This uses just 1 breakpoint and caches the entire static prefix. The tradeoff: if pinned skills change, the entire group's cache is invalidated. Whether that matters depends on how often you change pinned skills mid-conversation. ## The 4-Breakpoint Limit Anthropic enforces a hard limit of 4 `cache_control` blocks per API request. Iris maps each cache group (an array entry with a `cache` key) to one breakpoint. **How to count your breakpoints:** Count the number of array entries in your `prompts` config that have a `cache` key. That's your breakpoint count. ```php 'prompts' => [ ['cache' => [...], 'prompts' => [...]], // Breakpoint 1 ['cache' => [...], 'prompts' => [...]], // Breakpoint 2 SomePrompt::class, // No breakpoint ['cache' => [...], 'prompts' => [...]], // Breakpoint 3 AnotherPrompt::class, // No breakpoint ], // Total: 3 breakpoints ✓ ``` **What happens if you exceed 4:** Iris doesn't throw an error. The `SystemPromptBuilder` applies cache breakpoints to the **first 4 groups** that have a `cache` key and silently skips caching for any additional groups. Those extra groups still render their prompts normally — they just don't get cached. **Empty groups don't count:** If all prompts in a cached group return empty content, no `cache_control` block is produced and no breakpoint is consumed. This is why the default config can safely include the pinned skills group even when no skills are pinned — when both `PinnedSkillsPrompt` and `PinnedPromptsPrompt` return empty, that group uses zero breakpoints. > \[!WARNING] > If you see the error "A maximum of 4 blocks with cache\_control may be provided" from the Anthropic API, it means your config has more than 4 groups with `cache` keys that are all producing non-empty content. Remove caching from less important groups or consolidate prompts into fewer groups. ## Best Practices **Group static content together.** Prompts that share the same stability cadence should share a group. If three prompts all change only when you redeploy, put them in one group with `'ttl' => '1h'` — one breakpoint, maximum cache reuse. **Put the most stable content first.** Caching is prefix-based. If content at position 2 changes, everything after it (positions 3, 4, 5...) misses cache too, even if it hasn't changed. The default config puts the core identity prompt first because it essentially never changes. **Don't cache dynamic content.** Memories, summaries, calendar events, and current time change with every request. Caching them costs extra on the write side and never gets a hit on the read side. Keep these as standalone string entries. **Fewer breakpoints is simpler.** You don't need to use all 4 breakpoints. One breakpoint on the last static prompt already caches everything before it. Add more breakpoints only when you have content at different stability cadences (e.g., core identity changes yearly, pinned skills change weekly). **Non-Anthropic providers ignore cache config.** If you're using OpenAI, Ollama, or another provider, the cache groups and breakpoints in your config are completely harmless — they're just ignored. You can keep the grouped config format and switch providers without changing anything. > \[!TIP] > When in doubt, start with the default config. It uses 2 breakpoints and covers the common case well. Only add more groups if you have substantial custom static content that you want cached at a different TTL. ## Troubleshooting ### "A maximum of 4 blocks with cache\_control" Error This means the Anthropic API received more than 4 `cache_control` blocks. Count the groups in your `prompts` config that have a `cache` key. If more than 4 of them are producing non-empty content, you've exceeded the limit. **Fix:** Consolidate cached groups or remove caching from less critical groups. Remember that empty groups don't count, so a group might be under the limit in some cases (no pinned skills) but exceed it in others (skills pinned and all groups producing content). ### Cache Not Hitting If you're paying full input token cost on every request despite having cache groups configured, the content before a breakpoint is likely changing between requests. Common causes: * A prompt in a cached group includes dynamic data (timestamps, counters, randomly ordered lists) * Prompt content includes data that's fetched fresh on every request * The user changed pinned skills, invalidating the Group 2 cache **Fix:** Move any prompt with per-request content out of cached groups and into a standalone entry. Only truly stable content should be in a cached group. ### Using a Non-Anthropic Provider Cache groups are Anthropic-specific. If you're using OpenAI, Ollama, Gemini, or any other provider, the `cache` keys in your config are silently ignored. Your prompts still render in order and work correctly — you just don't get the caching optimization. This is by design. You can keep the grouped config format even when using non-Anthropic providers, which makes it easy to switch between providers without restructuring your prompt config. ### Custom Messages With Cache Options If you've written a custom prompt that returns messages with `cacheType` or `cacheTtl` provider options (via an `asMessages()` override), those values are **automatically stripped** by the `SystemPromptBuilder`. The config is the single authority for cache placement. This prevents individual prompts from accidentally exceeding the 4-breakpoint limit. --- --- url: /core-concepts/chat-interface.md --- # Chat Interface The chat interface orchestrates the full conversation flow — assembling context, streaming responses, executing tools, and persisting messages. Understanding this flow helps you see how all of Iris's components work together. Every conversation happens within a [thread](/core-concepts/threads). Threads provide isolation — conversation history, summaries, streaming, and pinned skills are all scoped to the active thread. ## Request Flow When you send a message, Iris processes it through several stages before streaming a response: ### 1. Context Recall The [ContextRetriever](/core-concepts/memory-system) fetches relevant context using two complementary systems: * **[Truths](/core-concepts/truths)**: Stable, core facts about you (pinned Truths always included, others ranked by relevance) * **Memories**: Semantic search finds memories related to the current conversation This happens first because context influences how Iris responds. ### 2. Context Assembly Multiple context sources are gathered in parallel: * Recent conversation history from this thread (up to 50 messages) * Conversation summaries from this thread (up to 3 recent summaries) * [Thread briefs](/core-concepts/thread-briefs) from other active threads (for cross-thread awareness) * [Pinned skill](/tools/agent-skills#pinning-skills-to-threads) content for this thread (if any) * Calendar events (next 7 days, if connected) * The current date and time ### 3. System Prompt Building The [SystemPromptBuilder](/core-concepts/system-prompts) assembles a personalized prompt by rendering each registered prompt class in order. The result includes Iris's identity, recalled memories, summaries, calendar context, and temporal information. ### 4. Process and Stream The request is queued for asynchronous processing via Prism PHP. Response events stream to your browser via WebSockets in real-time, including text chunks and tool calls. If Iris decides to use a tool, execution happens server-side before continuing the response. Stream events are scoped to the active thread — only clients viewing that thread receive the broadcast. This prevents messages from appearing in the wrong thread when you have multiple tabs open. This architecture provides reliable delivery with automatic recovery from connection drops. ### 5. Persist and Process After the response completes: * The conversation is saved to the database within the active thread * Token usage is recorded for monitoring * Background jobs are dispatched (memory extraction, thread-scoped summarization) ## Agentic Behavior Iris operates as an agent, meaning it can use tools and iterate multiple times before providing a final response. The `agent.max_steps` configuration (default: 60) limits how many iterations can occur. This enables complex, multi-step tasks: * **Search and ask**: Search memories, find nothing relevant, then ask a clarifying question * **Create and remember**: Create a calendar event, then store a memory about why it was scheduled * **Generate and describe**: Generate an image, then describe what was created Each tool invocation is a step. A simple memory storage is 1 step. A complex request might use 5-10 steps as Iris searches, creates, and confirms. > \[!NOTE] > Tool calls stream to the frontend in real-time, so users see what Iris is doing as it works. ## Features ### Text-to-Speech Assistant messages include a play button that reads the message aloud using ElevenLabs. Click once to play, click again to stop. This feature requires setup - see [Text-to-Speech](/integrations/text-to-speech) for configuration. ### Image Support Users can upload images for multi-modal conversations. Images are attached to messages and sent to Claude as part of the request. Common use cases include: * Asking questions about screenshots * Getting feedback on designs * Extracting information from photos ### Retry If a provider error occurs, the failed message appears inline with a retry button. Clicking **retry** re-runs the full request — context assembly, LLM call, and tool execution — with the same input. See [Error Handling](#error-handling) for details on what information is shown and when retrying helps. ### Streaming All responses stream in real-time via WebSockets using Laravel Reverb. Streams are scoped to the active [thread](/core-concepts/threads), so each thread's broadcasts are isolated. The stream includes: | Event Type | Content | |------------|---------| | Text chunks | Partial response text as it's generated | | Tool calls | When Iris invokes a tool | | Tool results | What the tool returned | | Provider tools | Activity from Anthropic's built-in tools | | Artifacts | Generated content like images | ### Connection Recovery If your connection drops briefly, you won't miss any events: * Events are temporarily stored in Redis for replay * Clients automatically catch up when reconnecting * Sequence numbers ensure no duplicates ### Stopping Streams Users can stop a stream mid-generation. The stop is graceful—partial responses are preserved and the conversation state remains consistent. ## Request Architecture When you send a message, Iris processes it asynchronously: 1. **Accept & Queue**: Your message is received within the active thread and a background job is dispatched 2. **Process**: The job builds thread-scoped context, executes the agent, and generates a response 3. **Broadcast**: Response events stream to clients viewing this thread via WebSockets 4. **Persist**: The conversation is saved to the thread and background jobs (extraction, summarization) are queued This architecture enables: * **Reliable delivery**: If your connection drops, events are stored and replayed when you reconnect * **Fail-fast errors**: Provider errors surface immediately with no automatic retries — the error appears inline with a retry button * **Non-blocking responses**: Your browser remains responsive while processing happens server-side ## Error Handling When a provider request fails, Iris does not retry automatically — the job runs once (`tries = 1`), and if it fails, the error surfaces immediately. Iris marks the conversation as failed and broadcasts the error to your browser. ### What you see The failed assistant message appears inline in the chat with two pieces of information: * **Human-readable message**: A plain-English description of what went wrong * **Retry button**: Always available — all errors are currently marked as recoverable ### Error messages by type | Error | Message shown | |-------|---------------| | Prompt too long (context window exceeded) | "The conversation is too long for the model's context window. Try starting a new conversation or asking Iris to work in smaller steps." | | Rate limited | "The AI provider is currently rate-limited. Please wait a moment and try again." | | Provider overloaded | "The AI provider is temporarily overloaded. Please try again in a moment." | | Other provider errors | The provider's own message, or a generic fallback | ### Retrying manually Click the **retry** button below a failed message to re-run the full request with the same input. For transient errors (rate limits, provider overload), retrying immediately or after a brief wait usually resolves the issue. ### Prompt too long If Iris reports that the conversation is too long for the model's context window, retrying with the same input won't help — the problem is the accumulated context size, not a transient provider state. Your options are: * **Start a new conversation** in a fresh thread with no history * **Ask Iris to work in smaller steps** — break the task into pieces that each fit within the window > \[!NOTE] > Context management (compaction, pruning, and progressive truncation) keeps most conversations well within bounds during normal use. See [Context Management](/core-concepts/context-management) for how Iris handles a filling context window before this limit is reached. ### Failed conversations are preserved Failed conversations are never deleted. The message stays in the chat with its error state and the retry button visible — even after navigating away and returning. Iris retains the full failed state so you always see what was attempted and what went wrong. ## Frontend Integration The frontend uses Laravel Echo for WebSocket connections. A custom hook manages connection state, event sequencing, and automatic replay on reconnection. --- --- url: /core-concepts/command-bar.md --- # Command Bar The command bar is a keyboard-driven command palette for quickly navigating Iris, managing threads, and accessing skills and prompts — without reaching for the mouse. Open it with Cmd+K (Mac) or Ctrl+K (Windows/Linux). Press the same shortcut or Esc to close it. ## Navigation The command bar gives you instant access to every section of Iris: | Destination | Description | |-------------|-------------| | Insights | Analytics and usage dashboard | | Memories | Semantic memory browser | | Truths | Core facts and knowledge | | Conversations | Thread list | | Prompts | Saved prompts | | Agent Tasks | Delegated task overview | | Guidance | Rules and guides | | Follow-ups | Reminders | | Logs | Debug information | Select any destination and press Enter to navigate there immediately. ## Thread Management The command bar provides full thread management without leaving your current conversation: * **Search Threads** — find and switch between threads with fuzzy search * **New Thread** — create a fresh thread * **Pin/Unpin Current Thread** — toggle the pinned status of your active thread * **Rename Current Thread** — edit the thread name inline * **Delete Current Thread** — remove the active thread (with confirmation) These actions are only available under the Threads category. Thread-specific actions like pin, rename, and delete require an active thread. ## Skills and Prompts When you have an active thread, two additional categories appear in the command bar: * **Skills** — browse available skills and pin or unpin them for the current thread * **Prompts** — browse your saved prompts and attach or detach them from the current thread Pinned skills and attached prompts are reflected immediately in the thread's settings. This is the fastest way to configure what context Iris uses for a specific conversation. ## Searching Start typing at any point to filter results with fuzzy search. The command bar searches intelligently based on where you are: * **At the top level** — searches across categories, navigation destinations, and thread actions * **Inside a category** — searches only within that category's items * **In thread search** — filters threads by name The fuzzy matching algorithm scores results by how well characters align with the start of words, rewarding matches at the beginning of text and after word boundaries. ## Keyboard Controls The command bar is fully keyboard-driven: | Key | Action | |-----|--------| | Cmd+K / Ctrl+K | Open or close the command bar | | ↑ ↓ | Move through items | | Enter | Select the highlighted item | | Esc | Go back one level, or close if at the top | | Backspace | Go back when the search field is empty | Arrow keys wrap around — pressing ↓ on the last item jumps to the first. ## Breadcrumb Navigation As you drill into categories and actions, a breadcrumb trail shows your current position: * **Threads > Search** — browsing threads * **Threads > Rename** — editing a thread name * **Threads > Delete** — confirming thread deletion * **Skills** — browsing available skills Press Esc or Backspace (with an empty search) to navigate back through the breadcrumb trail. --- --- url: /getting-started/configuration.md --- # Configuration Iris is configured through `config/iris.php`. All settings have sensible defaults, but understanding what they control helps you tune the system for your needs. ## How Configuration Works The base configuration lives in `config/iris.php`. To customize without modifying core files, create `config/iris-custom.php` and override specific values. See [Customization](/advanced/customization) for the full details on merge strategies. ```php // config/iris-custom.php return [ 'temporal' => [ 'timezone' => 'America/Chicago', // Default for all users ], ]; ``` > \[!TIP] > Individual users can override the default timezone in Settings > Profile. ## Providers Every Iris subsystem that calls an LLM has a configurable `provider` setting. By default, Iris uses Anthropic for chat and background tasks, OpenAI for embeddings, and ElevenLabs for text-to-speech. You can override any of these to use a different [Prism provider](https://prismphp.com) — including local providers like Ollama. ```php // config/iris-custom.php return [ 'agent' => [ 'provider' => 'ollama', 'model' => 'llama3.1:8b', ], ]; ``` Provider values are strings matching Prism's registered provider names (e.g., `anthropic`, `openai`, `ollama`, `gemini`, `mistral`, `groq`, `xai`, `deepseek`). See the [Local Setup Guide](/advanced/local-setup) for running Iris entirely with local models. ## Agent Settings These control the core chat behavior. | Setting | Default | Description | |---------|---------|-------------| | `agent.provider` | anthropic | Prism provider for chat | | `agent.model` | claude-sonnet-4-5 | The model used for chat | | `agent.max_steps` | 60 | Maximum tool iterations per request | **What `max_steps` controls**: When Iris uses tools, it can iterate multiple times before responding. A request like "schedule a meeting and remind me later" might use 2-3 tools. The default of 60 allows for complex multi-step tasks while preventing runaway loops. ## Context Settings These control what information Iris has access to when responding. | Setting | Default | Description | |---------|---------|-------------| | `context.recent_summaries` | 3 | Number of conversation summaries included | **How context builds**: Each request includes recent messages from the active [thread](/core-concepts/threads), recent summaries from that thread, [pinned skill](/tools/agent-skills#pinning-skills-to-threads) content, recalled memories, and calendar events. History is loaded token-budget-first — Iris pulls turns newest-first until the available token budget is exhausted. ## Context Management Iris uses a token-budget system to keep conversation history within model limits. After accounting for system prompts, tool definitions, and reserved headroom, it fills the remaining space with as much history as fits — starting from the most recent messages. ### Token Budget Settings | Setting | Default | Description | |---------|---------|-------------| | `context.window` | 200000 | The context window size (in tokens) of the configured model | | `context.token_budget_ratio` | 0.70 | Fraction of the context window allocated to conversation history | | `context.compaction_threshold` | 0.75 | Token usage fraction that triggers compaction before the next LLM request | | `context.prune_protect_turns` | 2 | Number of recent turns whose tool outputs are shielded from compaction pruning | **`window`**: Set this to match your configured model's context window. For example, Claude Sonnet 4.6 and Opus 4.7 have 1M token context windows — set this to `1000000`. The default of `200000` is safe for Haiku 4.5 and Sonnet 4.5. **`token_budget_ratio`**: Controls how much of the context window is reserved for history. At the default of `0.70`, a 200K window allocates 140K tokens for conversation messages. Raise this if you want more history; lower it if system prompts and tool definitions are large. **`compaction_threshold`**: When actual token usage reaches this fraction of the context window, Iris compacts the context before the next request — summarizing older messages to free space. At `0.75`, compaction triggers at 75% utilization. Lower this to compact more aggressively; raise it to allow the context to fill further before compacting. **`prune_protect_turns`**: During compaction, tool call outputs from the most recent N turns are never pruned, even if they're large. This ensures the agent retains the context of its most recent actions. Increase this if agents seem to lose track of recent tool results. You can override `token_budget_ratio` via environment variable: ```bash # .env IRIS_CONTEXT_TOKEN_BUDGET_RATIO=0.80 ``` ### Filesystem Tool Limits These settings cap how much output filesystem tools return per call. Limiting output keeps individual responses from consuming a disproportionate share of the context window. | Setting | Default | Description | |---------|---------|-------------| | `filesystem.max_read_lines` | 500 | Maximum lines returned per file read | | `filesystem.max_read_chars` | 30000 | Maximum characters returned per file read | | `filesystem.max_grep_matches` | 100 | Maximum matches returned per grep search | When output exceeds a limit, Iris truncates the response and saves the full result to disk. The truncation message tells the agent the saved file path so it can re-read specific sections using offset and limit parameters. ### Tool Output Storage Truncated tool outputs are saved to disk so agents can access the full content on demand. | Setting | Default | Description | |---------|---------|-------------| | `tool_output.storage_path` | `storage/app/private/tool-output/` | Directory where full truncated outputs are saved | | `tool_output.ttl` | 86400 | Seconds before saved output files are eligible for cleanup (24 hours) | Saved files are cleaned up automatically on a daily schedule. You can also run cleanup manually: ```bash php artisan iris:prune-tool-outputs ``` ## Truths Settings [Truths](/core-concepts/truths) are stable, core facts about you that persist across conversations. Unlike memories that are retrieved contextually, Truths represent distilled knowledge earned through behavioral evidence. | Setting | Default | Description | |---------|---------|-------------| | `truths.max_dynamic` | 7 | Maximum non-pinned Truths included per conversation | | `truths.percentile_threshold` | 5 | Top N% of memories by access count are distillation candidates | | `truths.min_total_memories` | 20 | Minimum memories required before distillation runs | | `truths.similarity_threshold` | 0.40 | Minimum relevance score for a Truth to be included | | `truths.duplicate_threshold` | 0.80 | Similarity threshold to detect duplicate Truths | | `truths.stale_days` | 90 | Days without access before a Truth is considered stale | | `truths.crystallization_provider` | anthropic | Prism provider for crystallization | | `truths.crystallization_model` | claude-sonnet-4-5 | Model for refining Truths with new evidence | | `truths.promotion_provider` | anthropic | Prism provider for promotion evaluation | | `truths.promotion_model` | claude-sonnet-4-5 | Model for analyzing promotion candidates | | `truths.jobs_per_minute` | 10 | Rate limit for distillation API calls | **Pinned vs Dynamic Truths**: You can pin any Truth to ensure it's always included, regardless of relevance scoring. The `max_dynamic` setting only limits non-pinned Truths - pinned Truths are unlimited. Run distillation manually: ```bash php artisan iris:distill-truths --queue # Process all users php artisan iris:distill-truths --user=1 --queue # Process specific user php artisan iris:distill-truths --dry-run # Preview without changes ``` ## Memory Settings Memory settings control how Iris retrieves contextual information during conversations. | Setting | Default | Description | |---------|---------|-------------| | `memory.max_results` | 7 | Maximum memories from semantic search | | `memory.similarity_threshold` | 0.38 | Minimum similarity score to include a memory | | `memory.context_turns` | 20 | Conversation turns used to generate search queries | | `memory.recall_provider` | anthropic | Prism provider for recall query generation | | `memory.recall_model` | claude-sonnet-4-5 | Model for generating memory search queries | **How memory retrieval works**: When you start a conversation, Iris generates search queries based on recent messages and finds semantically similar memories. Only memories above the similarity threshold are included, keeping context focused on what's relevant. ### Scoring Weights Memories are ranked by a composite score: ``` (semantic × 0.60) + (recency × 0.25) + (frequency × 0.15) ``` | Weight | Factor | What it means | |--------|--------|---------------| | `semantic` | 0.60 | How relevant to the current conversation | | `recency` | 0.25 | Decays linearly over 90 days | | `frequency` | 0.15 | How often the memory has been accessed | Certain memory types receive bonuses: relationships (+0.10), preferences and goals (+0.05), and recent events (+0.10). > \[!TIP] > If memories seem stale, increase the `recency` weight. If relevant memories aren't surfacing, increase `semantic` or lower `similarity_threshold`. ## Extraction Settings Extraction controls how memories are automatically created from conversations. | Setting | Default | Description | |---------|---------|-------------| | `extraction.threshold` | 10 | Messages between extraction runs | | `extraction.max_memories` | 6 | Maximum memories per extraction | | `extraction.history_limit` | 50 | Recent conversation messages passed to the extraction LLM | | `extraction.timeout` | 120 | API timeout in seconds | | `extraction.provider` | anthropic | Prism provider for extraction | | `extraction.model` | claude-sonnet-4-5 | Model for analyzing conversations | **The quality vs quantity tradeoff**: A lower `max_memories` forces the extraction model to be selective, producing higher-quality memories. Increasing it may capture more information but risks storing trivial details. ## Summarization Settings Summarization compresses older messages to preserve context without consuming too many tokens. | Setting | Default | Description | |---------|---------|-------------| | `summarization.threshold` | 40 | Unsummarized messages before triggering | | `summarization.buffer` | 35 | Recent messages excluded from summarization | | `summarization.timeout` | 120 | API timeout in seconds | | `summarization.provider` | anthropic | Prism provider for summary generation | | `summarization.model` | claude-sonnet-4-5 | Model for generating summaries | **How summarization triggers**: Summarization is now primarily triggered by the token budget — when context utilization reaches `context.compaction_threshold`, Iris compacts older messages into summaries to free space. The `threshold` setting acts as a secondary message-count guard: if unsummarized messages exceed it, summarization also runs. During compaction, the most recent `buffer` messages are always preserved intact. > \[!WARNING] > Very aggressive compaction (low `compaction_threshold`) may lose conversational nuance. The defaults balance context preservation with token efficiency. ## Consolidation Settings Consolidation merges semantically similar memories into denser, more useful representations over time. | Setting | Default | Description | |---------|---------|-------------| | `consolidation.similarity_threshold` | 0.80 | Minimum cosine similarity to cluster | | `consolidation.days_lookback` | 3 | Days to look back for daily runs | | `consolidation.max_memories_per_run` | 100 | Rate limit per consolidation run | | `consolidation.max_cluster_size` | 5 | Max memories per cluster | | `consolidation.min_cluster_size` | 2 | Min memories to form a cluster | | `consolidation.max_generation` | 5 | Max consolidation generations | | `consolidation.timeout` | 120 | API timeout in seconds | | `consolidation.provider` | anthropic | Prism provider for consolidation decisions | | `consolidation.model` | claude-sonnet-4-5 | Model for merge decisions | | `consolidation.jobs_per_minute` | 10 | Rate limit for API calls | **Understanding generations**: Original memories are Generation 0. When merged, the result is Generation 1. Memories can be re-consolidated up to Generation 5, allowing them to evolve as more related information is gathered. See [Memory Consolidation](/core-concepts/memory-consolidation) for details. Run consolidation manually: ```bash php artisan iris:consolidate-memories php artisan iris:consolidate-memories --dry-run # Preview without changes ``` ## Truth Consolidation Settings Truth consolidation merges semantically similar Truths to reduce redundancy. Unlike memory consolidation which runs daily, truth consolidation runs weekly since Truths accumulate more slowly. | Setting | Default | Description | |---------|---------|-------------| | `truth_consolidation.similarity_threshold` | 0.75 | Minimum cosine similarity to cluster | | `truth_consolidation.max_cluster_size` | 5 | Max Truths per cluster | | `truth_consolidation.min_cluster_size` | 2 | Min Truths to form a cluster | | `truth_consolidation.timeout` | 120 | API timeout in seconds | | `truth_consolidation.provider` | anthropic | Prism provider for truth consolidation | | `truth_consolidation.model` | claude-sonnet-4-5 | Model for merge decisions | | `truth_consolidation.jobs_per_minute` | 10 | Rate limit for API calls | **Protection model**: Only Promoted Truths are eligible for consolidation. User-created, agent-created, and pinned Truths are protected. Truths at the maximum generation are also excluded. The threshold (0.75) is lower than memory consolidation (0.80) because Truths are already distilled facts - semantic overlap is more likely to be true redundancy. Run consolidation manually: ```bash php artisan iris:consolidate-truths php artisan iris:consolidate-truths --dry-run # Preview without changes ``` See [Truth Consolidation](/core-concepts/truth-consolidation) for details on how it works. ## Calendar Settings | Setting | Default | Description | |---------|---------|-------------| | `calendar.cache_ttl` | 15 | Cache duration in minutes | | `calendar.event_horizon` | 7 | Days ahead to include in context | **Why caching matters**: Calendar data is fetched from Google's API and cached to avoid repeated requests. The `event_horizon` controls how far ahead Iris looks -a larger value gives more scheduling context but increases the data in each request. ## Embeddings Settings | Setting | Default | Description | |---------|---------|-------------| | `embeddings.provider` | openai | Prism provider for embeddings | | `embeddings.model` | text-embedding-3-small | Embedding model | | `embeddings.provider_options` | `[]` | Provider-specific options passed to Prism | Embeddings power semantic search. The default `text-embedding-3-small` model offers a good balance of quality and cost. Changing the model or provider affects how memories are stored and retrieved. The `provider_options` array lets you pass provider-specific configuration. For example, when using Ollama for embeddings you'll want to set dimensions to match the default vector size: ```php 'embeddings' => [ 'provider' => 'ollama', 'model' => 'your-embedding-model', 'provider_options' => [ 'dimensions' => 1536, ], ], ``` ## Text-to-Speech Settings > \[!NOTE] > Text-to-speech is disabled by default. Enable it by setting `IRIS_TTS_ENABLED=true` in your `.env`. | Setting | Default | Description | |---------|---------|-------------| | `tts.enabled` | `false` | Enable TTS audio playback | | `tts.provider` | `elevenlabs` | Prism provider for text-to-speech | | `tts.model` | `eleven_multilingual_v2` | TTS model | | `tts.voice` | (configured) | Voice ID for speech generation | When enabled, assistant messages display a play button that generates audio using ElevenLabs. See [Text-to-Speech](/integrations/text-to-speech) for setup instructions. ## Telegram Notification Settings > \[!NOTE] > Telegram notifications are disabled by default. Enable them by setting `TELEGRAM_ENABLED=true` in your `.env` and configuring a bot token. | Setting | Environment Variable | Description | |---------|---------------------|-------------| | `connectors.telegram.enabled` | `TELEGRAM_ENABLED` | Enable Telegram integration | | `connectors.telegram.token` | `TELEGRAM_BOT_TOKEN` | Bot token from BotFather | | `connectors.telegram.bot_username` | `TELEGRAM_BOT_USERNAME` | Bot username for generating connect links | These settings live in `config/connectors.php`. When enabled, proactive messages are pushed to users who've linked their Telegram account and toggled notifications on. See [Telegram Notifications](/integrations/telegram-notifications) for the full setup guide. ## Thread Briefs Settings [Thread Briefs](/core-concepts/thread-briefs) give Iris cross-thread awareness by generating lightweight digests of each thread's current state. | Setting | Default | Description | |---------|---------|-------------| | `briefs.enabled` | `true` | Master toggle for all brief-related behavior | | `briefs.frequency` | `2` | Assistant responses between brief generations | | `briefs.threshold` | `2` | Minimum assistant responses before first brief | | `briefs.max_threads` | `5` | Maximum threads shown in cross-thread context | | `briefs.provider` | `anthropic` | Prism provider for brief generation | | `briefs.model` | `claude-haiku-4-5` | Model for generating briefs | | `briefs.timeout` | `15` | Timeout in seconds for generation | **How it works:** After every N assistant responses (default: 2), a background job generates a 3-6 sentence digest covering the thread's topic, key decisions, current status, and emotional context. These briefs are injected into other threads' system prompts and used by the heartbeat for context. The master `enabled` toggle controls everything — generation, cross-thread injection, and heartbeat usage. Disable via environment variable: ```bash # .env IRIS_BRIEFS_ENABLED=false ``` ### Customizing Briefs ```php // config/iris-custom.php return [ 'briefs' => [ 'frequency' => 3, // Generate less often 'max_threads' => 3, // Fewer threads in cross-thread context ], ]; ``` ## Proactive Messages Settings | Setting | Default | Description | |---------|---------|-------------| | `heartbeat.provider` | `anthropic` | Prism provider for heartbeat decisions | | `heartbeat.model` | `claude-sonnet-4-5` | Model for heartbeat decisions | | `heartbeat.max_steps` | `30` | Maximum tool iterations when crafting messages | | `heartbeat.history_limit` | `null` | Conversation messages included (null = use default) | | `heartbeat.context_max_threads` | `3` | Most recently active threads to include in heartbeat context | | `heartbeat.prompts` | *(see below)* | [Prompt stack](/core-concepts/system-prompts#heartbeat-prompt-stack) for heartbeat system messages | **How it works:** Iris runs a heartbeat every 30 minutes, gathering context (thread briefs and latest summaries from active threads, thread metadata, calendar, weather, memories) and deciding whether to proactively reach out. The heartbeat uses a [dedicated prompt stack](/core-concepts/system-prompts#heartbeat-prompt-stack) that's lighter than the full conversation prompt stack. Users configure guidance (soft preferences) and boundaries (hard constraints like quiet hours) through the UI. See [Proactive Messages](/core-concepts/proactive-messages) for detailed usage and configuration. ## Temporal Settings | Setting | Default | Description | |---------|---------|-------------| | `temporal.timezone` | America/New\_York | Default timezone for date/time context | This timezone is injected into the system prompt so Iris knows the current date and time. It serves as the **system-wide default** for all users. ### Per-User Timezone Each user can override the default timezone in **Settings > Profile**. When a user sets their timezone, it's used everywhere: system prompts, heartbeat scheduling, quiet hours, follow-up validation, and the frontend. The fallback chain is: 1. **User's timezone** (if set in their profile) 2. **Config default** (`temporal.timezone`) If a user leaves timezone blank, the config default applies automatically. This makes the config value a sensible default for new users while letting individuals customize. ## Shell Settings > \[!WARNING] > Shell command execution is disabled by default. Enable only on trusted deployments. | Setting | Default | Description | |---------|---------|-------------| | `shell.enabled` | `false` | Enable shell command execution | | `shell.default_timeout` | `30` | Default timeout in seconds | | `shell.max_timeout` | `300` | Maximum allowed timeout (5 minutes) | | `shell.max_output_length` | `50000` | Maximum output size in bytes | | `shell.default_working_directory` | `null` | Default directory for commands | | `shell.blocked_executables` | `['sudo', ...]` | Blocked command names | | `shell.blocked_patterns` | `[...]` | Regex patterns to block | | `shell.inherit_env_vars` | `['PATH', ...]` | Environment variables to inherit | Enable via environment variable: ```bash # .env IRIS_SHELL_ENABLED=true ``` **Security layers:** Privilege escalation commands are blocked (`sudo`, `su`, `doas`, `pkexec`). Dangerous patterns like `rm -rf /` and direct disk writes are detected and blocked. Only safe environment variables are inherited. See [Shell Commands](/tools/shell-commands) for detailed usage and security information. ## Broadcasting Settings Iris uses Laravel Reverb for real-time streaming. Configure via environment variables: | Setting | Default | Description | |---------|---------|-------------| | `BROADCAST_CONNECTION` | reverb | Broadcasting driver | | `REVERB_APP_ID` | - | Application identifier | | `REVERB_APP_KEY` | - | Client authentication key | | `REVERB_APP_SECRET` | - | Server-side secret | | `REVERB_HOST` | localhost | Reverb server hostname | | `REVERB_PORT` | 8080 | Reverb server port | | `REVERB_SCHEME` | http | Protocol (http or https for production) | ### Event Storage Stream events are temporarily stored in Redis for connection recovery. Events remain available for replay, allowing clients to catch up after brief disconnections. ## Sub-Agent Settings > \[!WARNING] > Task delegation is disabled by default. Requires shell commands to also be enabled. | Setting | Default | Description | |---------|---------|-------------| | `subagent.enabled` | `false` | Enable task delegation | | `subagent.provider` | `anthropic` | Prism provider for the sub-agent | | `subagent.model` | `claude-sonnet-4-5` | Model for the sub-agent | | `subagent.max_steps` | `30` | Maximum tool iterations per task | | `subagent.default_working_directory` | `null` | Default directory for tasks | | `subagent.request_timeout` | `120` | HTTP timeout per API request | | `subagent.default_timeout` | `300` | Default task timeout (5 minutes) | | `subagent.max_timeout` | `600` | Maximum task timeout (10 minutes) | Enable via environment variables: ```bash # .env IRIS_SUBAGENT_ENABLED=true IRIS_SHELL_ENABLED=true IRIS_SUBAGENT_WORKING_DIR=/home/user/projects # Optional default directory ``` **How it works:** Task delegation spawns an autonomous sub-agent using Prism's agent loop. The sub-agent has access to shell commands and web tools, executing multi-step tasks independently before returning results. Tasks are processed by the `iris:agent` daemon — see [Task Delegation](/tools/task-delegation#running-the-agent-daemon) for setup. **Constraints:** One active task per user to prevent resource exhaustion. Inherits all shell security settings. See [Task Delegation](/tools/task-delegation) for detailed usage information. ## Token Usage Tracking Iris tracks API token consumption across all operations. You can monitor usage by source type: | Source | What it tracks | |--------|----------------| | `chat` | Main conversation tokens | | `summarization` | Summary generation tokens | | `extraction` | Memory extraction tokens | | `consolidation` | Memory consolidation tokens | | `truth_consolidation` | Truth consolidation tokens | | `promotion` | Truth promotion analysis tokens | | `crystallization` | Truth crystallization tokens | | `embedding` | Embedding API calls | Token data is stored in the `token_usages` table and can be queried for cost analysis or monitoring dashboards. ## Common Configuration Scenarios ### Lower API Costs ```php // config/iris-custom.php return [ 'extraction' => [ 'threshold' => 20, // Extract less frequently ], 'summarization' => [ 'threshold' => 60, // Summarize less often ], 'truths' => [ 'max_dynamic' => 3, // Fewer dynamic Truths per conversation ], 'memory' => [ 'max_results' => 5, // Fewer search results ], ]; ``` ### More Responsive Memory ```php // config/iris-custom.php return [ 'extraction' => [ 'threshold' => 5, // Extract more frequently ], 'memory' => [ 'max_results' => 10, // More search results 'similarity_threshold' => 0.10, // Lower threshold ], ]; ``` ### Conservative Memory Consolidation ```php // config/iris-custom.php return [ 'consolidation' => [ 'similarity_threshold' => 0.90, // Only very similar memories 'max_generation' => 3, // Limit consolidation depth ], ]; ``` ### Conservative Truth Consolidation ```php // config/iris-custom.php return [ 'truth_consolidation' => [ 'similarity_threshold' => 0.85, // Only very similar truths ], ]; ``` ### Aggressive Truth Promotion ```php // config/iris-custom.php return [ 'truths' => [ 'percentile_threshold' => 20, // Wider candidate pool 'min_total_memories' => 10, // Start promoting sooner ], ]; ``` ### Enable Shell Commands ```php // config/iris-custom.php return [ 'shell' => [ 'default_timeout' => 60, 'default_working_directory' => '/home/user/projects', ], ]; ``` Then set `IRIS_SHELL_ENABLED=true` in your `.env`. ### Enable Task Delegation ```php // config/iris-custom.php return [ 'subagent' => [ 'max_steps' => 50, 'default_timeout' => 600, // 10 minutes for complex tasks 'default_working_directory' => '/home/user/projects', ], ]; ``` Then set both `IRIS_SUBAGENT_ENABLED=true` and `IRIS_SHELL_ENABLED=true` in your `.env`. ### Optimize for 200K Models For models with 200K context windows (Claude Haiku 4.5, Claude Sonnet 4.5), keep context lean to avoid hitting limits during long conversations. ```php // config/iris-custom.php return [ 'context' => [ 'window' => 200_000, 'token_budget_ratio' => 0.65, // Leave more headroom for system prompts 'compaction_threshold' => 0.70, // Compact earlier to avoid hitting limits 'prune_protect_turns' => 2, ], 'filesystem' => [ 'max_read_lines' => 300, // Smaller reads to conserve context 'max_read_chars' => 15000, 'max_grep_matches' => 50, ], ]; ``` ### Maximize Context on 1M Models For models with 1M context windows (Claude Sonnet 4.6, Claude Opus 4.7), you can afford much larger history and tool outputs. ```php // config/iris-custom.php return [ 'context' => [ 'window' => 1_000_000, 'token_budget_ratio' => 0.80, // Use more of the window for history 'compaction_threshold' => 0.85, // Allow context to fill further before compacting 'prune_protect_turns' => 5, // Protect more recent tool outputs ], 'filesystem' => [ 'max_read_lines' => 2000, // Read much larger files in one shot 'max_read_chars' => 100000, 'max_grep_matches' => 500, ], ]; ``` ## Thread Settings [Threads](/core-concepts/threads) organize conversations into isolated contexts with independent history and summaries. | Setting | Default | Description | |---------|---------|-------------| | `threads.naming_threshold` | 4 | Messages before auto-naming triggers | | `threads.naming_provider` | `anthropic` | Prism provider for naming | | `threads.naming_model` | `claude-haiku-4-5` | Model for generating thread names | | `threads.naming_timeout` | 15 | Timeout in seconds for the naming job | **How auto-naming works**: After a new thread accumulates enough messages (default: 4), a background job generates a short, descriptive name. A fast, inexpensive model is used to keep costs minimal. If a user manually renames a thread, auto-naming won't overwrite their choice. ### Customizing Thread Naming ```php // config/iris-custom.php return [ 'threads' => [ 'naming_threshold' => 6, // Wait for more conversation context ], ]; ``` ## Skills Settings | Setting | Default | Description | |---------|---------|-------------| | `skills.enabled` | `true` | Enable agent skills | | `skills.directory` | `.agents/skills` | Directory to load skills from | Skills extend Iris with specialized knowledge and workflows. You can [pin skills to threads](/tools/agent-skills#pinning-skills-to-threads) to inject their full content into the system prompt for every message in that thread. Pinned skills are configured per-thread through the thread settings modal. See [Agent Skills](/tools/agent-skills) for details. --- --- url: /core-concepts/context-management.md --- # Context Management Every request Iris sends to an LLM has a fixed token limit — the model's context window. Context management is the set of mechanisms Iris uses to keep that request within bounds while preserving as much relevant history as possible. This page covers how the token budget is calculated, how conversation history is loaded against that budget, how tool output pruning reclaims space invisibly, and how compaction summarizes old turns when the window fills up. ## Token Budget Calculation Before loading any history, Iris calculates how many tokens are available. The formula is: ``` available tokens = (context window × token_budget_ratio) - system prompt tokens - tool definition tokens ``` The token estimate uses a **chars/4 heuristic**: `ceil(strlen($text) / 4)`. This isn't perfect, but it's fast and errs conservatively. ### Config keys | Key | Default | Effect | |-----|---------|--------| | `context.window` | `200000` | The context window size (in tokens) of your configured model | | `context.token_budget_ratio` | `0.70` | Fraction of the context window allocated to conversation history | Set `context.window` to match the model you're using. For example, Claude Sonnet 4.6 and Opus 4.7 have 1M token context windows — set `context.window` to `1000000` to take advantage of the full capacity. The default of 200K is safe for models like Haiku 4.5 and Sonnet 4.5. On a 200K window, after accounting for a typical system prompt and tool definitions (~10K tokens combined), you'll have ~130K tokens for conversation history. On a 1M window, that grows to ~690K tokens — roughly 5× more conversation depth. > \[!TIP] > To allow more history at the cost of less headroom for responses, raise `context.token_budget_ratio` to `0.80` or `0.85`. This can help on smaller context windows during long tool-heavy sessions. ## Budget-Based History Loading Iris loads conversation history starting from the most recent turn and works backwards, accumulating turns until the token budget is exhausted. Unlike a message-count approach, this treats turns by size, not by number. A conversation with 50 long tool outputs and 50 short exchanges would consume wildly different amounts of context — the token-budget approach accounts for that. The model always sees the most recent turns first, never truncating from the middle. **How history loading works:** 1. Query all conversations from the active thread, newest-first 2. Calculate available token budget using `TokenBudgetCalculator` 3. Iterate through conversations, accumulating token estimates, until the budget is exhausted 4. Reverse the selected messages to chronological order before passing to the LLM ### Config keys | Key | Default | Effect | |-----|---------|--------| | `context.token_budget_ratio` | `0.70` | Controls how many tokens are available for history | ## Tool Output Pruning Even with budget-based loading, tool outputs from older turns can waste tokens. A `read_file` call that returned 5,000 characters of file content is useful context when it happens — but ten turns later, the agent probably doesn't need all of that content verbatim. **Pruning** replaces the content of old tool results with a placeholder (`[Tool output cleared]`) in the LLM request. It happens at request-build time, not as a background job, and it's completely invisible to you: full content is always retained in the database. ### What gets pruned Every `ToolResultMessage` outside the most recent N turns is a pruning candidate. The N most recent turns are always protected. A **turn** is one user message plus all subsequent assistant messages and tool result messages until the next user message. With `context.prune_protect_turns = 2`, the two most recent turns are shielded — their tool outputs are sent verbatim. All older turns have their tool outputs replaced with the placeholder. ### Config keys | Key | Default | Effect | |-----|---------|--------| | `context.prune_protect_turns` | `2` | Number of recent turns whose tool outputs are never pruned | ### The invariant: database content is always preserved Pruning operates exclusively on in-memory Prism message objects. It never modifies the `conversations` table. You can always inspect the full tool output of any past turn directly in the database — pruning only affects what's sent to the LLM for a given request. > \[!IMPORTANT] > Full conversation content — including all tool outputs — is always retained in the database regardless of pruning or compaction state. ## Compaction When the context window fills up despite pruning, Iris compacts the conversation. Compaction summarizes older turns into a structured narrative, freeing the window for new activity. ### When compaction triggers Before each LLM request, Iris checks the `prompt_tokens` from the most recent completed turn against the model's context window: ``` if prompt_tokens >= context_window × compaction_threshold → compact ``` At the default `compaction_threshold` of `0.75`, compaction triggers when the last request used 75% or more of the model's context window. This means compaction fires *before* the window overflows, not after. ### What the summary includes Compaction generates a structured `ConversationSummary` with these fields: | Field | Description | |-------|-------------| | `summary` | 150–300 word narrative of the conversation segment | | `accomplishments` | Completed tasks and outcomes from this segment | | `key_decisions` | Choices made and conclusions reached | | `relevant_files` | Files touched, modified, or referenced | | `active_goals` | Open goals or tasks still in progress | | `emotional_thread` | How the emotional tone evolved through this segment | | `relationship_dynamics` | Shifts in formality, rapport, and trust | | `evolving_themes` | Topics that developed or transformed across turns | Recent turns are kept verbatim — compaction only affects the turns being summarized, not the tail that will still appear in full. For a deeper look at what summaries capture and how the summary chain works, see [Summarization](/core-concepts/summarization). ### Config keys | Key | Default | Effect | |-----|---------|--------| | `context.compaction_threshold` | `0.75` | Token usage fraction that triggers compaction | | `context.prune_protect_turns` | `2` | Recent turns kept verbatim during compaction | ### Progressive truncation fallback If the compacted summary plus protected recent turns still exceed the context window, Iris applies progressive fallback: 1. Reduce the protected turn window step-by-step (configured value → 1 → 0) 2. If still over budget with all tool outputs pruned, throw `ConversationTooLargeException` This is a last resort — normal compaction almost never reaches step 2. ## Walkthrough: A Long Tool-Heavy Conversation Here's how the full lifecycle plays out in practice. **Setup**: You're running on a 200K context model. System prompt + tool definitions occupy ~10K tokens. The budget ratio is 0.70, so ~130K tokens are available for history. *** **Turns 1–5: Early conversation, tools used freely** The budget check shows 130K tokens available. History loading grabs all 5 turns — they fit easily. Pruning protects the 2 most recent turns; turns 1–3 have their tool outputs replaced with `[Tool output cleared]`. But this is invisible: turns 1–3 still appear in the database in full. *** **Turns 6–20: Tool-heavy accumulation** You ask Iris to explore a large codebase — multiple `read_file` calls, grep searches, shell commands. Each turn produces thousands of characters of tool output. History loading can still fit all 20 turns because pruning shrinks the effective token cost of older turns. The protected window (last 2 turns) always has full tool output; everything older is a placeholder. *** **Turn 21: Compaction triggers** The `prompt_tokens` reported back from the last request is 158K — above the 75% threshold of 150K. Before sending turn 21 to the LLM, Iris runs the Summarizer on the oldest turns not yet summarized. The result is a `ConversationSummary` covering turns 1–18, capturing accomplishments (files explored, patterns found), relevant files (the ones you looked at), active goals (the refactoring task still in progress), and emotional thread (curiosity, some frustration at a confusing module). Turns 19–21 are kept in full detail. The LLM now sees: * The new summary injected via the system prompt * Turns 19–21 in full (with pruning applied to 19's tool outputs) * Your new message (turn 21) *** **Turn 22 onward: Conversation continues** The context is spacious again. History loading now fits turns 19–21 plus any new turns. As the conversation grows, the cycle repeats: pruning reclaims token space turn by turn, and compaction fires again when utilization creeps back up toward 75%. *** **What you see**: Nothing. The conversation flows naturally. Iris remembers the files you explored and the goals you set even after they've been summarized away from the raw message list. ## Cross-References * [Summarization](/core-concepts/summarization) — what conversation summaries capture and how the summary chain works * [Configuration: Context Management](/getting-started/configuration#context-management) — full reference for all config keys: `context.token_budget_ratio`, `context.compaction_threshold`, `context.prune_protect_turns` --- --- url: /tools/custom-tools.md --- # Custom Tools Extend Iris's capabilities by creating custom tools that integrate with your services and data. Custom tools have full access to Laravel's ecosystem -databases, APIs, queues, and more. ## Tool Anatomy Every tool is a class that extends Prism's `Tool` base class. Here's the structure: ```php as('tool_name') // Name Iris uses to call it ->for('Description of what the tool does') // Helps Iris decide when to use it ->withStringParameter('param', 'Description') // Define parameters ->using($this); // Tell Prism to use __invoke } public function __invoke(string $param): string { // Your tool logic here return 'Result message'; } } ``` For complete documentation on parameter types, return values, and advanced patterns, see the [Prism Tools Documentation](https://prismphp.com/core-concepts/tools-function-calling). ## Registering Tools Register your tools in `config/iris-custom.php`. Your tools are appended to the core tools: ```php return [ 'tools' => [ App\Extensions\Tools\FetchNotesTool::class, App\Extensions\Tools\WeatherTool::class, ], ]; ``` To disable core tools you don't need: ```php return [ 'disabled_tools' => [ App\Tools\GenerateImageTool::class, App\Tools\Calendar\CreateCalendarEventTool::class, ], ]; ``` ## Injecting the User Iris resolves tools from Laravel's container with the authenticated user bound. This lets you scope tool behavior to the current user automatically: ```php as('fetch_notes') ->for('Fetch the user\'s saved notes') ->using($this); } public function __invoke(): string { $notes = $this->user ->notes() ->latest() ->take(10) ->get(); if ($notes->isEmpty()) { return 'No notes found.'; } return $notes ->map(fn ($note) => "- {$note->title}: {$note->content}") ->join("\n"); } } ``` You can also inject any service registered in Laravel's container: ```php public function __construct( protected User $user, protected WeatherService $weather, protected CacheManager $cache, ) { // ... } ``` ## Return Values Tools return strings that Iris incorporates into its response. Follow these guidelines: ### Success Responses Be concise and informative. Include relevant details Iris can relay to the user: ```php // Good return "Note created: '{$note->title}' (ID: {$note->id})"; // Too verbose return "The note creation operation completed successfully. The note with the title '{$note->title}' has been saved to the database with ID {$note->id}. You can now reference this note in future conversations."; ``` ### Empty Results Clearly indicate when no results were found: ```php if ($notes->isEmpty()) { return 'No notes found.'; } // Or with more context if ($notes->isEmpty()) { return 'No notes found matching that search. Try different keywords.'; } ``` ### Error Responses Return error messages as strings -don't throw exceptions unless something is truly broken. Iris can explain the error to the user: ```php public function __invoke(string $location): string { $apiKey = $this->user->settings->weather_api_key; if (! $apiKey) { return 'Weather API key not configured. Add one in Settings > Integrations.'; } try { return $this->weather->current($location, $apiKey); } catch (RateLimitException $e) { return 'Weather service rate limit reached. Try again in a few minutes.'; } catch (Throwable $e) { report($e); // Log the actual error return 'Unable to fetch weather data. The service may be temporarily unavailable.'; } } ``` > \[!TIP] > Return user-friendly messages. Iris will present these to the user, so "Weather API key not configured" is better than "Missing WEATHER\_API\_KEY environment variable." ## Complete Example: Task Management Tool Here's a complete example showing a tool that manages tasks: ```php as('create_task') ->for('Create a new task or todo item for the user') ->withStringParameter('title', 'Task title') ->withStringParameter('description', 'Task description (optional)') ->withStringParameter('dueDate', 'Due date in YYYY-MM-DD format (optional)') ->withStringParameter('priority', 'Priority: low, medium, or high (default: medium)') ->using($this); } public function __invoke( string $title, string $description = '', ?string $dueDate = null, string $priority = 'medium', ): string { // Validate priority if (! in_array($priority, ['low', 'medium', 'high'])) { return "Invalid priority '{$priority}'. Use low, medium, or high."; } // Parse due date if provided $parsedDueDate = null; if ($dueDate) { try { $parsedDueDate = Carbon::parse($dueDate); } catch (Throwable) { return "Couldn't parse due date '{$dueDate}'. Use YYYY-MM-DD format."; } } // Create the task $task = $this->user->tasks()->create([ 'title' => $title, 'description' => $description, 'due_date' => $parsedDueDate, 'priority' => $priority, ]); $response = "Task created: '{$task->title}'"; if ($parsedDueDate) { $response .= " (due {$parsedDueDate->format('M j, Y')})"; } return $response; } } ``` ## Testing Tools Test tools by binding a user to the container and resolving the tool: ```php has(Note::factory()->count(3)) ->create(); app()->instance(User::class, $user); $tool = resolve(FetchNotesTool::class); $result = $tool(); expect($result)->toContain($user->notes->first()->title); }); it('handles users with no notes', function () { $user = User::factory()->create(); app()->instance(User::class, $user); $tool = resolve(FetchNotesTool::class); $result = $tool(); expect($result)->toBe('No notes found.'); }); it('respects the limit parameter', function () { $user = User::factory() ->has(Note::factory()->count(20)) ->create(); app()->instance(User::class, $user); $tool = resolve(FetchNotesTool::class); $result = $tool(limit: 5); // Only 5 notes should appear expect(substr_count($result, '- '))->toBe(5); }); ``` ## Tool Description Best Practices The `for()` description helps Iris decide when to use your tool. Make it clear and specific: ```php // Good - specific about what it does ->for('Fetch the user\'s saved notes, optionally filtered by tag') // Good - mentions when to use it ->for('Get current weather conditions for a location. Use when user asks about weather.') // Bad - too vague ->for('Handle notes') // Bad - too technical ->for('Executes SELECT query against notes table with pagination') ``` ## Organizing Tools For complex applications, organize tools into subdirectories: ``` app/ ├── Extensions/ │ └── Tools/ │ ├── Notes/ │ │ ├── FetchNotesTool.php │ │ ├── CreateNoteTool.php │ │ └── DeleteNoteTool.php │ ├── Tasks/ │ │ ├── ListTasksTool.php │ │ └── CreateTaskTool.php │ └── Weather/ │ └── GetWeatherTool.php ``` Register them all in config: ```php return [ 'tools' => [ App\Extensions\Tools\Notes\FetchNotesTool::class, App\Extensions\Tools\Notes\CreateNoteTool::class, App\Extensions\Tools\Notes\DeleteNoteTool::class, App\Extensions\Tools\Tasks\ListTasksTool::class, App\Extensions\Tools\Tasks\CreateTaskTool::class, App\Extensions\Tools\Weather\GetWeatherTool::class, ], ]; ``` --- --- url: /advanced/customization.md --- # Customization Iris is designed to be customized. This guide covers modifying the AI personality, adding integrations, adjusting truth and memory behavior, and maintaining your changes through upgrades. ## Customization Philosophy Iris separates "core" from "custom" to help you extend the application without forking: * **Core files** live in `app/`, `config/iris.php`, and `resources/views/prompts/` * **Custom files** go in `app/Extensions/`, `config/iris-custom.php`, and your own directories This separation means you can: * Pull updates to Iris without losing your customizations * Clearly see what you've changed vs what's stock * Share customizations as standalone packages ## Custom Configuration Create a `config/iris-custom.php` file to override settings, add tools, or customize prompts without modifying core files. This is your application configuration -commit it to version control with the rest of your customizations. ```php // config/iris-custom.php [ 'provider' => 'anthropic', // Override the default provider 'model' => 'claude-opus-4', // Override the default model ], 'truths' => [ 'max_dynamic' => 8, // Override specific settings ], ]; ``` Every subsystem that calls an LLM has its own `provider` and `model` settings, so you can mix and match providers across different tasks. For example, you could use Anthropic for chat but Ollama for background tasks like memory extraction. See [Configuration: Providers](/getting-started/configuration#providers) for the full list. ### How Merging Works Different config keys use different merge strategies: | Key | Strategy | Description | |-----|----------|-------------| | `prompts` | Replace | Your list replaces the core list entirely. Entries can be cache groups (arrays with `cache` and `prompts` keys) or standalone class names (strings). See [Cache Breakpoints](/core-concepts/cache-breakpoints). | | `tools` | Append | Your tools are added to core tools | | `disabled_tools` | Filter | Listed tools are removed from the final set | | `provider_tools` | Replace | Your list replaces core provider tools | | Everything else | Smart merge | Associative arrays merge recursively; indexed arrays replace entirely | **Smart merge details:** For nested configuration like `shell` or `truths`, associative keys are merged recursively (your values override core values). However, indexed arrays (lists) like `blocked_executables`, `blocked_patterns`, or `inherit_env_vars` are replaced entirely—your list becomes the new list, rather than being combined with the core list. ```php // config/iris-custom.php return [ 'shell' => [ // This scalar value overrides the core value 'default_timeout' => 60, // This indexed array REPLACES the core list entirely 'blocked_executables' => ['sudo'], // Only 'sudo' is blocked, not the full core list // Omitted keys like 'blocked_patterns' keep their core values ], ]; ``` ### Adding Custom Tools Add your own tools without modifying core config: ```php // config/iris-custom.php return [ 'tools' => [ App\Extensions\Tools\WeatherTool::class, App\Extensions\Tools\HomeAssistantTool::class, ], ]; ``` Your tools are appended to the core tools, so Iris still has truths, memory, calendar, and image generation. ### Disabling Tools Remove tools you don't need: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\GenerateImageTool::class, App\Tools\Calendar\CreateCalendarEventTool::class, App\Tools\Shell\RunShellCommandTool::class, // Disable shell even if env enabled App\Tools\Agent\DelegateTaskTool::class, // Disable task delegation ], ]; ``` ### Customizing Prompts The `prompts` array defines system prompt classes organized into [cache breakpoint groups](/core-concepts/cache-breakpoints). To customize, you **replace the entire list** — both the groups and standalone entries (order matters for system prompts): ```php // config/iris-custom.php return [ 'prompts' => [ // Cached group — static content with 1h TTL [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, ], ], // Cached group — pinned content [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, ], ], // Standalone entries — dynamic, per-request content App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Extensions\Prompts\WorkContextPrompt::class, // Your custom prompt App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` Array entries with `cache` and `prompts` keys are cache groups — each produces one cache breakpoint. String entries are standalone prompts with no caching. See [Cache Breakpoints](/core-concepts/cache-breakpoints) for the full details on groups, TTL options, and the 4-breakpoint limit. Create your own prompt class by extending the base. Prompts are self-contained: they inject `RequestContext` and any services they need, then fetch their context in `content()`: ```php // app/Extensions/Prompts/WorkContextPrompt.php $this->projectService->getActiveProjects( $this->requestContext->user()?->id ), ]); } } ``` > \[!IMPORTANT] > Prompt caching is managed entirely through config groups, not in prompt classes. Don't set `cacheType` or `cacheTtl` in `providerOptions()` — those values are automatically stripped. To cache a prompt, place it inside a group with a `cache` key in the config. The `RequestContext` provides access to the current user and message: ```php $this->requestContext->user(); // Current User model (or null) $this->requestContext->message(); // Current user message $this->requestContext->images(); // Attached images array ``` ### Customizing Provider Tools Provider tools (like Anthropic's web search and web fetch) are stored as arrays for config caching compatibility: ```php // config/iris.php default format 'provider_tools' => [ ['type' => 'web_fetch_20250910', 'name' => 'web_fetch'], ['type' => 'web_search_20250305', 'name' => 'web_search'], ], ``` To disable all provider tools: ```php // config/iris-custom.php return [ 'provider_tools' => [], // Empty array disables all ]; ``` To use only specific provider tools: ```php // config/iris-custom.php return [ 'provider_tools' => [ ['type' => 'web_search_20250305', 'name' => 'web_search'], // web_fetch disabled ], ]; ``` ## Complete Customization Example Here's a complete example of customizing Iris for a development team assistant that tracks projects and integrates with internal tools. ### 1. Create the Custom Config ```php [ 'provider' => 'anthropic', 'model' => 'claude-sonnet-4-5', ], // More aggressive context settings for work 'truths' => [ 'max_dynamic' => 8, // More Truths per conversation ], 'memory' => [ 'max_results' => 10, // More semantic search results ], // Custom tools for internal systems 'tools' => [ App\Extensions\Tools\JiraIntegration\ListTicketsTool::class, App\Extensions\Tools\JiraIntegration\CreateTicketTool::class, App\Extensions\Tools\Slack\SendMessageTool::class, App\Extensions\Tools\GitHub\ListPullRequestsTool::class, ], // Disable image generation (not needed for dev work) 'disabled_tools' => [ App\Tools\GenerateImageTool::class, ], // Custom prompt pipeline with work context 'prompts' => [ // Cached group — custom identity + core behavior [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Extensions\Prompts\DevTeamStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, ], ], // Cached group — pinned content [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, ], ], // Dynamic content — changes per request App\Prompts\MemoryPrompt::class, App\Prompts\SummaryPrompt::class, App\Extensions\Prompts\ProjectContextPrompt::class, App\Extensions\Prompts\TeamContextPrompt::class, App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], // Default timezone (users can override in their profile) 'temporal' => [ 'timezone' => 'America/Los_Angeles', ], ]; ``` ### 2. Create Custom Prompts ```php render(); } } ``` ```blade {{-- resources/views/prompts/extensions/dev-team-static.blade.php --}} # Iris - Development Team Assistant You are Iris, a technical assistant for the engineering team at Acme Corp. Your role is to help developers stay productive by: - Tracking project status and deadlines - Managing Jira tickets and GitHub pull requests - Facilitating team communication via Slack - Remembering technical decisions and context ## Communication Style Be direct and technical. Skip pleasantries when discussing code or issues. Use precise terminology. When referencing tickets, include the ID (e.g., ACME-123). ## Team Standards - All code changes require PR review - Tickets move through: To Do → In Progress → Review → Done - Sprint planning is every Monday at 10am - Deployments happen on Tuesdays and Thursdays ``` ### 3. Create Custom Tools See [Custom Tools](/tools/custom-tools) for the full guide. Here's a quick example: ```php as('list_jira_tickets') ->for('List Jira tickets assigned to the user or in current sprint') ->withStringParameter('filter', 'Filter: mine, sprint, or project key (default: mine)') ->using($this); } public function __invoke(string $filter = 'mine'): string { $tickets = match ($filter) { 'mine' => $this->jira->getAssignedTo($this->user->email), 'sprint' => $this->jira->getCurrentSprint(), default => $this->jira->getByProject($filter), }; if ($tickets->isEmpty()) { return 'No tickets found.'; } return $tickets ->map(fn ($t) => "- [{$t->key}] {$t->summary} ({$t->status})") ->join("\n"); } } ``` ### 4. Register Services If your tools need external services, register them in a service provider: ```php app->singleton(JiraClient::class, function () { return new JiraClient( baseUrl: config('services.jira.url'), apiToken: config('services.jira.token'), ); }); } } ``` ## Upgrade Strategy When Iris releases updates, follow this process to incorporate them while preserving your customizations. ### Before Upgrading 1. **Review the changelog** - Check for breaking changes or new features 2. **Backup your database** - Especially if migrations are included 3. **Note your customizations** - List what you've changed ### Upgrade Process ```bash # Fetch the latest changes git fetch origin # Review what's changed git diff main..origin/main # Merge or rebase git merge origin/main # or git rebase origin/main # If conflicts arise in customized files, resolve them # Your custom config (iris-custom.php) shouldn't conflict # Custom classes in app/Extensions/ shouldn't conflict # Run migrations php artisan migrate # Rebuild frontend if needed npm run build # Clear caches php artisan config:clear php artisan view:clear ``` ### Handling Conflicts **Core prompts changed**: If you've replaced the prompts array and core prompts changed, review the changes and update your custom config if needed. **New config options**: New options in `iris.php` are automatically available. Override in `iris-custom.php` if you need different values. **Database migrations**: Run migrations after every upgrade. Review migration files if you've modified the schema. ### Recommended Git Strategy Keep your customizations on a separate branch: ```bash # Create a customizations branch git checkout -b customizations main # Make your customizations # Commit them to the customizations branch # When upgrading: git checkout main git pull origin main git checkout customizations git rebase main ``` --- --- url: /tools/filesystem-tools.md --- # Filesystem Tools > \[!IMPORTANT] > This feature is in **beta** and represents an initial implementation. The API and behavior may change or be removed in future releases. Iris can read, write, search, and list files within a confined workspace directory. The five filesystem tools give Iris structured access to a single workspace root per deployment — isolated from the rest of the host filesystem by design. > \[!WARNING] > Filesystem tools are **disabled by default**. Enable them only on trusted deployments where you understand the access implications. ## Setup ### Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `IRIS_FILESYSTEM_ENABLED` | No | `false` | Set to `true` to enable all filesystem tools | | `IRIS_WORKSPACE_ROOT` | No | `storage/app/iris/workspace` | Absolute path to the workspace root directory | Add the following to your `.env` file: ```env IRIS_FILESYSTEM_ENABLED=true # Optional: override the default workspace location IRIS_WORKSPACE_ROOT=/var/data/iris/workspace ``` ### Workspace Root Behavior **Default path (`storage/app/iris/workspace`):** If `IRIS_WORKSPACE_ROOT` is not set, Iris uses `storage/app/iris/workspace` relative to the application root. This directory is **created automatically** on the first tool call if it does not already exist. **User-configured path:** If `IRIS_WORKSPACE_ROOT` is set to a directory that does not exist, Iris will **not** create it. Instead, every tool call will return an error until the directory is created manually. This avoids silently writing to an unintended location. *** ## Tools ### `read_file` **Class:** `App\Tools\Filesystem\ReadFileTool` Read the contents of a file within the workspace. Returns the file content with 1-based line numbers in `cat -n` style (` 1\tline content`). Registers the path as "read" in the session, which is required before `write_file` can overwrite an existing file or `edit_file` can modify a file. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to the file (e.g. `notes/todo.md`) | | `offset` | integer | No | 1-based line number to start reading from. Bypasses the max-bytes threshold when provided. | | `limit` | integer | No | Maximum number of lines to return (default: 2000). Bypasses the max-bytes threshold when provided. | **Returns:** File content with line numbers, or an error view if the path is rejected, the file does not exist, or binary content is detected. #### Output Truncation Large files are automatically truncated to protect the LLM's context window. Two limits apply — whichever is hit first wins: | Config Key | Default | Description | |------------|---------|-------------| | `iris.filesystem.max_read_lines` | `500` | Maximum number of lines sent to the LLM | | `iris.filesystem.max_read_chars` | `30000` | Maximum number of characters sent to the LLM | When truncation occurs, a notice is appended after the file content: ``` Showing lines 1-500 of 3,000. Use the offset and limit parameters to read specific sections. Full output saved to /path/to/storage/app/private/tool-output/. Use ReadFileTool with offset/limit to access specific sections. ``` The full file content is always written to the tool output storage path (`iris.tool_output.storage_path`, default `storage/app/private/tool-output/`). The absolute path to that file is included in the truncation notice so the agent can reference or re-read it directly using `read_file` with `offset` and `limit`. > \[!NOTE] > Truncation is an LLM context optimization only. The full content is always retained in the database and in the saved output file. Your conversation history is unaffected — the agent can read any section of a large file by calling `read_file` again with `offset` and `limit` parameters. *** ### `write_file` **Class:** `App\Tools\Filesystem\WriteFileTool` Write content to a file within the workspace. Creates the file and any intermediate directories if they do not exist. When writing to an **existing** file, the file must have been read in the current session via `read_file` first (read-first gate). | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to the target file | | `content` | string | Yes | Full file content to write (replaces any existing content) | **Returns:** Confirmation with workspace-relative path and byte count written, or an error view if the path is rejected or the read-first gate blocks the write. *** ### `edit_file` **Class:** `App\Tools\Filesystem\EditFileTool` Perform an exact-string replacement on a file within the workspace. The file must have been read in the current session via `read_file` first (read-first gate). | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Workspace-relative path to the file to edit | | `old_string` | string | Yes | Exact string to find and replace. Must appear in the file. Must differ from `new_string`. | | `new_string` | string | Yes | Replacement string. Must differ from `old_string`. | | `replace_all` | boolean | No | When `false` (default), `old_string` must be unique — multiple matches return an error. When `true`, all occurrences are replaced. | **Returns:** Confirmation of the edit with replacement count, or an error view if the path is rejected, the read-first gate blocks the edit, the strings are identical, or `old_string` is ambiguous without `replace_all`. *** ### `grep` **Class:** `App\Tools\Filesystem\GrepTool` Search file contents using a regular expression pattern within the workspace. Supports three output modes and optional scoping by subdirectory or file type. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pattern` | string | Yes | Regular expression to search for (PHP regex syntax, without delimiters) | | `path` | string | No | Workspace-relative subdirectory to restrict the search scope. Defaults to the workspace root. | | `glob` | string | No | Glob pattern matched against workspace-relative file paths to filter which files are searched (e.g. `**/*.php`) | | `output_mode` | string | No | `files_with_matches` (default) — returns matching file paths only; `content` — returns `path:line: text` prefixed matching lines; `count` — returns match counts per file | **Returns:** Matching files, lines, or counts in the chosen format. Returns a human-readable no-matches message when zero files match. #### Output Truncation Search results are capped at a configurable match limit to protect the LLM's context window: | Config Key | Default | Description | |------------|---------|-------------| | `iris.filesystem.max_grep_matches` | `100` | Maximum number of matches sent to the LLM | When the cap is hit, a notice is appended after the results: ``` Showing 100 of 500 matches. Use a more specific pattern or path to narrow results. Full output saved to /path/to/storage/app/private/tool-output/. Use ReadFileTool with offset/limit to access specific sections. ``` The full match output is always written to the tool output storage path (`iris.tool_output.storage_path`). The absolute path to that file is included in the truncation notice. > \[!NOTE] > Truncation is an LLM context optimization only. The full output is always retained in the database and in the saved output file. Your conversation history is unaffected — narrow the search with a more specific pattern or `path` to get targeted results within the cap. *** ### `glob` **Class:** `App\Tools\Filesystem\GlobTool` List files matching a glob pattern within the workspace. Use to discover files by name, extension, or directory structure. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pattern` | string | Yes | Glob pattern to match against workspace-relative paths (e.g. `**/*.md`, `src/**/*.php`) | | `path` | string | No | Workspace-relative subdirectory to restrict the search scope. Defaults to the workspace root. | **Returns:** Lexicographically sorted list of matching workspace-relative paths, or an error view if the pattern or path is rejected. *** ## Containment Model All five tools share a single workspace root per deployment, enforced by `App\Services\Filesystem\WorkspacePathResolver`. Iris resolves and validates every path before any filesystem operation occurs: * **Tilde expansion:** Iris treats `~` as the workspace root, not the system user's home directory. A path like `~/notes.md` resolves to `{workspace_root}/notes.md`. A lone `~` resolves to the workspace root itself. * **Absolute paths:** Iris rejects any path beginning with `/` unless it falls within the workspace root. All other paths must be workspace-relative. * **Traversal sequences:** Iris blocks any `..` segments that would resolve outside the workspace root — including paths like `../../etc/passwd` and patterns like `../**/*`. * **Symlinks:** If a symlink target resolves to a location outside the workspace root, Iris rejects the path. * **Realpath resolution:** For existing paths, Iris uses PHP's `realpath()` to resolve the final absolute path and checks it against the workspace root. For not-yet-existing paths (e.g. a new file to write), Iris normalises `..` segments manually before the containment check. Iris logs every rejection via `Log::warning` with the attempted input, rejection reason, and authenticated user ID (if any). Logs never include the absolute workspace root. *** ## Denylist The denylist (`iris.filesystem.blocked_paths`) lets you prevent Iris from accessing specific files or directories within the workspace, even when the path passes the containment check. Patterns in the denylist are matched against the **post-resolution absolute realpath** of the requested file using PHP's `fnmatch`. For example, a pattern of `*.env` would block any file whose realpath ends in `.env`. The default denylist is empty. Add patterns in `config/iris-custom.php`: ```php // config/iris-custom.php return [ 'filesystem' => [ 'blocked_paths' => [ '*.env', '*.key', 'secrets/*', ], ], ]; ``` > \[!TIP] > Denylist patterns are matched against the full resolved absolute path, so patterns like `secrets/*` should be written relative to what `fnmatch` will receive — the absolute path. Use `*` to match within a directory at any level (e.g. `*/secrets/*`). *** ## Limitations * **Text files only.** The tools do not support binary files, images, or PDFs. `read_file` detects non-UTF-8 content by sampling the first 8 192 bytes and returns an error without reading further. * **Max read size.** `read_file` enforces a maximum file size of **1 MB** (`iris.filesystem.max_read_bytes`, default `1048576`) when called without `offset` or `limit`. Providing either parameter bypasses this threshold, allowing chunked reads of larger files. * **LLM output caps.** Even within the 1 MB limit, `read_file` truncates output sent to the LLM at 500 lines (`iris.filesystem.max_read_lines`) or 30,000 characters (`iris.filesystem.max_read_chars`), whichever comes first. `grep` caps results at 100 matches (`iris.filesystem.max_grep_matches`). Full output is always saved to the tool output storage path and the path is included in the truncation notice — use `offset` and `limit` on a follow-up `read_file` call to page through large files. * **Read-first gate for `edit_file` and overwrite `write_file`.** Both tools require the target file to have been read via `read_file` in the same session before they will modify it. Writing to a *new* (non-existent) file bypasses this gate. * **Single root per deployment.** There is one workspace root for the entire deployment. Per-user scoping or multiple independent workspaces are not supported. --- --- url: /integrations/google-calendar.md --- # Google Calendar Integration Iris integrates with Google Calendar to give you conversational control over your schedule. Ask about upcoming events, create meetings, reschedule appointments, and manage your calendar through natural conversation. ## Features * **View Events**: See upcoming events and ask about your schedule * **Create Events**: Schedule meetings with natural language * **Update Events**: Modify times, titles, locations, and descriptions * **Delete Events**: Cancel events when plans change * **Multi-Calendar Support**: Work with multiple calendars ## Setup ### 1. Create Google Cloud Credentials 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select an existing one 3. Enable the **Google Calendar API** 4. Go to **Credentials** and create an **OAuth 2.0 Client ID** 5. Set the application type to **Web application** 6. Add your callback URL: `http://localhost:8000/settings/google/callback` ### 2. Configure Environment ```bash GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret ``` ### 3. Connect Your Account 1. Open Iris and go to **Settings** 2. Find the **Google Calendar** integration 3. Click **Connect** and authorize access 4. Select which calendars Iris should have access to ## Context Injection When connected, Iris automatically receives your calendar events in every conversation — both upcoming events and recent past events. This gives Iris awareness of what you've had going on, not only what's ahead. By default, Iris includes 3 days of past events and 7 days of future events. Both windows are configurable (see [Configuration](#configuration)). > **You**: "Can we meet tomorrow?" > > **Iris**: "I see you have a dentist appointment at 10am and a team standup at 2pm tomorrow. Would 11:30am or 4pm work better?" > **You**: "What did I have yesterday?" > > **Iris**: "Yesterday you had a 1:1 with Alex at 10am and a sprint review at 3pm." ## Available Tools | Tool | Purpose | |------|---------| | `list_calendar_events` | Fetch upcoming events (1-30 days ahead) | | `create_calendar_event` | Create new events with title, time, location | | `update_calendar_event` | Modify existing events | | `delete_calendar_event` | Remove events | ## Example Conversations > **You**: "What do I have this week?" > > **Iris**: "Here's your week: Monday 10am Team standup, Tuesday 2pm Client call, Wednesday 9am Dentist..." > **You**: "Schedule a meeting with Sarah Tuesday at 3pm about the roadmap" > > **Iris**: "Created: Project Roadmap Discussion with Sarah, Tuesday at 3:00 PM." > **You**: "Move my dentist appointment to Thursday" > > **Iris**: "Done - moved from Wednesday 9am to Thursday 9am." ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `calendar.cache_ttl` | 15 | Cache duration (minutes) | | `calendar.event_horizon` | 7 | Days ahead to include in context | | `calendar.event_lookback` | 3 | Days in the past to include in context. Set to `0` to disable. | ## Security Considerations ### Token Storage Google OAuth tokens are stored encrypted in the database (`google_token` and `google_refresh_token` fields on the User model). Laravel's encryption uses your `APP_KEY`, so keep that secure. ### Permissions Scope Iris requests the `https://www.googleapis.com/auth/calendar` scope, which provides full read/write access to calendars the user selects. Users choose which calendars to share during the connection flow. ### Token Refresh Access tokens expire after 1 hour. Iris automatically refreshes them using the refresh token before making API calls. If the refresh token becomes invalid (user revokes access, token expires after 6 months of inactivity), the user needs to reconnect. ## Troubleshooting **Calendar not connected**: Disconnect and reconnect in Settings, ensure all permissions are granted. **Events not showing**: Check that the calendar is selected in Settings and events fall within the configured look-back (3 days past) or event horizon (7 days ahead). **Can't create events**: Verify you have write access to the target calendar. **"Token has been expired or revoked"**: The refresh token is invalid. Disconnect and reconnect in Settings. **Events appearing in wrong timezone**: Iris uses ISO 8601 timestamps with timezone info. Check that your timezone is set correctly in Settings > Profile. The system default can also be configured in `config/iris.php` under `temporal.timezone`. --- --- url: /integrations/image-generation.md --- # Image Generation Iris can generate images from text descriptions using OpenAI's GPT Image model. Ask for artwork, illustrations, diagrams, or any visual content. ## Features * **Text-to-Image**: Generate images from natural language * **Multiple Sizes**: Square, landscape, or portrait * **High Quality**: Uses GPT Image with high quality settings * **Inline Display**: Images appear directly in chat * **Persistent Storage**: Images are saved and accessible later ## Setup Image generation requires an OpenAI API key. Verify your `.env` includes: ```bash OPENAI_API_KEY=your-openai-key ``` That's it -image generation is automatically available. The `generate_image` tool uses the same API key configured for embeddings. ## Usage Simply ask Iris to create an image: > **You**: "Create an image of a sunset over mountains" > > **Iris**: *\[generates and displays image]* > > "Here's a sunset over mountains. Would you like me to adjust anything?" ### Being Specific The more detail you provide, the better the results: > **You**: "Generate a watercolor painting of a cozy coffee shop with warm lighting, wooden furniture, plants on the windowsill, and a cat sleeping on a chair" ### Style Guidance Specify artistic styles to get the look you want: | Style | Example prompt | |-------|----------------| | Minimalist | "A minimalist line art portrait of a woman with flowing hair" | | Pixel art | "A pixel art scene of a forest at night with fireflies" | | Photorealistic | "A photorealistic image of a modern kitchen with marble countertops" | | Watercolor | "A watercolor painting of a sailboat at sunset" | | Oil painting | "An oil painting of a mountain landscape in the style of the Hudson River School" | | Digital art | "Digital art of a futuristic cityscape with neon lights" | ## Available Sizes | Size | Orientation | Use Case | |------|-------------|----------| | `1024x1024` | Square | General purpose, icons, avatars | | `1536x1024` | Landscape | Scenes, backgrounds, banners | | `1024x1536` | Portrait | Characters, posters, mobile wallpapers | If you don't specify a size, Iris defaults to 1024x1024. You can request a size naturally: > "Create a landscape banner of a beach scene" → Uses 1536x1024 > "Generate a portrait-oriented poster design" → Uses 1024x1536 ## How It Works 1. **Request**: Iris interprets your request and formulates a detailed prompt 2. **Generation**: The prompt is sent to GPT Image via OpenAI's API 3. **Download**: The generated image is downloaded from OpenAI's temporary URL 4. **Storage**: The image is saved locally as an attachment linked to the conversation 5. **Display**: The image appears inline in the chat ### Storage Details Generated images are stored as attachments with: * **Path**: Saved in local storage (configurable via Laravel's filesystem) * **Type**: Marked as `generated` (vs `upload` for user-uploaded images) * **Description**: The prompt used to generate the image * **Conversation link**: Associated with the message that requested it ## Prompt Enhancement Iris often enhances your prompts for better results: > **You**: "Draw a cat" > > *Becomes*: "A photorealistic image of a domestic cat with soft fur, sitting in natural lighting, detailed eyes, high quality" You can override this by being very specific or asking for exactly what you described: > "Generate exactly this: a simple sketch of a cat, no details, just basic outlines" ## Limitations and Constraints ### Content Policy OpenAI's content policy applies. Requests that violate the policy will be rejected: * No realistic images of public figures * No violent, adult, or harmful content * No content that could be used for deception If a request is rejected, Iris will explain why and suggest alternatives. ### Quality Considerations * **Text in images**: GPT Image can include text, but it may be imperfect. For text-heavy designs, consider post-processing. * **Specific details**: Very specific requests (exact layouts, precise proportions) may not render exactly as described. * **Consistency**: Multiple requests for "the same" thing will produce variations. There's no way to get identical outputs. ### Rate Limits and Costs Image generation uses OpenAI API credits: | Tier | Images per minute | Cost per image | |------|-------------------|----------------| | Free tier | Very limited | - | | Pay-as-you-go | Higher limits | ~$0.04-0.08 depending on size | Check your OpenAI dashboard for current pricing and usage. If you hit rate limits, Iris will report the error. ## Troubleshooting **"OpenAI API key not configured"**: Add `OPENAI_API_KEY` to your `.env` file. **"Content policy violation"**: The prompt was rejected by OpenAI's safety filters. Rephrase your request. **Image not appearing**: Check that the storage disk is configured correctly and writable. Images are stored using Laravel's default disk. **Slow generation**: Image generation typically takes 5-15 seconds. Very complex prompts may take longer. ## Disabling Image Generation If you don't need image generation, disable the tool: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\GenerateImageTool::class, ], ]; ``` --- --- url: /getting-started/installation.md --- # Installation ## Prerequisites * PHP 8.4+ * Node.js 20+ * Composer 2.x * Redis 6+ (for queues and real-time events) * Docker (recommended) or PostgreSQL 16+ with pgvector ## Database Setup ### Docker (Recommended) ```bash docker compose up -d ``` This creates PostgreSQL 16 with the pgvector extension and Redis. The setup includes both an `iris` database and a `testing` database for running tests. Default credentials: user `iris`, password `iris`, port `5432`. ### Manual PostgreSQL If you're running PostgreSQL directly, create the database and enable the vector extension: ```sql CREATE DATABASE iris; \c iris CREATE EXTENSION vector; ``` > \[!IMPORTANT] > The pgvector extension is required for semantic memory search. Without it, memory retrieval won't work. ## Application Installation ```bash git clone https://github.com/sixlive/iris.git cd iris composer install npm install cp .env.example .env php artisan key:generate ``` ## Environment Configuration Edit `.env` with your API keys: ```bash ANTHROPIC_API_KEY=your-anthropic-key OPENAI_API_KEY=your-openai-key # Optional - Google Calendar GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret ``` The `.env.example` file includes Reverb configuration for real-time communication: ```bash # Real-time Communication (Reverb) REVERB_APP_ID=iris-local REVERB_APP_KEY=iris-local-key REVERB_APP_SECRET=iris-local-secret REVERB_HOST=localhost REVERB_PORT=8080 REVERB_SCHEME=http # Frontend Reverb connection VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}" ``` These defaults work for local development. For production, update the host, port, and scheme to match your deployment. ## Run Migrations ```bash php artisan migrate ``` ## Build Frontend ```bash npm run build ``` ## Generate Invite Codes Iris uses invite-only registration to control access. Generate invite codes before users can register: ```bash # Generate a single invite code php artisan iris:generate-invite-codes # Generate multiple codes php artisan iris:generate-invite-codes 5 ``` Each code is a unique ULID that can only be used once. The codes are stored in the `invite_codes` table and marked as used when someone registers. ## Running Iris The easiest way to run Iris is with the dev command: ```bash composer dev ``` This starts the web server, Horizon (queue management), Reverb (WebSocket server), log viewer, and Vite dev server together. Visit `http://localhost:8000`. Horizon's dashboard is available at `http://localhost:8000/horizon`. ### Manual Startup If you prefer to run services separately: **Terminal 1**: `php artisan serve`\ **Terminal 2**: `php artisan horizon`\ **Terminal 3**: `php artisan reverb:start`\ **Terminal 4**: `php artisan iris:agent` (required for [task delegation](/tools/task-delegation))\ **Terminal 5**: `php artisan schedule:work` (optional, for scheduled consolidation) > \[!IMPORTANT] > Both Horizon and Reverb are essential. Horizon processes background jobs (memory extraction, summarization). Reverb enables real-time streaming of chat responses. Without them, chat won't work properly. ## User Registration Head to `/register` and create a new user using the invite code that you generated earlier. ![user registration](/images/registration.png) ## Optional: Google Calendar Setup Google Calendar integration lets Iris see your schedule and manage events. Setup requires creating OAuth credentials in Google Cloud. ### 1. Create Google Cloud Project 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select an existing one 3. Enable the **Google Calendar API**: * Go to **APIs & Services** > **Library** * Search for "Google Calendar API" * Click **Enable** ### 2. Configure OAuth Consent Screen 1. Go to **APIs & Services** > **OAuth consent screen** 2. Select **External** (unless you have a Google Workspace organization) 3. Fill in the required fields: * App name: "Iris" (or your preferred name) * User support email: Your email * Developer contact: Your email 4. Add the scope: `https://www.googleapis.com/auth/calendar` 5. Add yourself as a test user (required while app is in testing mode) ### 3. Create OAuth Credentials 1. Go to **APIs & Services** > **Credentials** 2. Click **Create Credentials** > **OAuth 2.0 Client ID** 3. Select **Web application** 4. Add authorized redirect URI: `http://localhost:8000/settings/google/callback` 5. Copy the Client ID and Client Secret ### 4. Add to Environment ```bash GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret ``` ### 5. Connect Your Account 1. Start Iris and log in 2. Go to **Settings** 3. Find the **Google Calendar** section and click **Connect** 4. Authorize access and select which calendars Iris should see ## Troubleshooting **Database connection refused**: If using Docker, ensure the container is running with `docker compose ps`. **pgvector extension not found**: With Docker, this is automatic. For manual installations, see the [pgvector installation guide](https://github.com/pgvector/pgvector#installation). **Queue jobs not processing**: Ensure `php artisan horizon` is running. Check for failed jobs in the Horizon dashboard at `/horizon` or with `php artisan queue:failed`. --- --- url: /getting-started/introduction.md --- # Introduction Iris is an AI-powered chat application with persistent semantic memory, built on Laravel 12 and powered by [Prism](https://prismphp.com). It remembers your conversations, learns your preferences, and connects to your calendar. ## Who Is Iris For? Iris is designed for developers and technical users who want: * **A personal AI that grows with them** - Most AI assistants start fresh every conversation. Iris builds a persistent understanding of who you are, what you're working on, and what matters to you. * **Control over their infrastructure** - Self-hosted means you own the application, database, and configuration. Your memories and conversation history stay on your server. By default, conversations are sent to Anthropic for processing and OpenAI handles embeddings, but every provider is configurable — you can [run Iris entirely locally](/advanced/local-setup) with Ollama or swap in any Prism-supported provider. * **A reference for building AI applications** - Iris demonstrates how to build sophisticated AI applications with Laravel and [Prism PHP](https://prismphp.com), including semantic memory, agentic tool use, and streaming responses. * **A reference for AI-assisted development** - The entire Iris codebase was built with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), serving as a practical example of developing Laravel applications with AI assistance. If you're curious about how to build semantic memory systems, implement agentic tool use, or integrate LLMs into Laravel applications, the Iris codebase is designed to be readable and instructive. If you're exploring AI-assisted development workflows, Iris demonstrates what's possible when building with Claude Code. ## What Makes Iris Different Traditional AI chat applications treat each conversation as isolated. Iris builds a persistent memory layer that grows smarter over time. ### Persistent Memory Iris automatically extracts important information and stores it as semantic memories. When you mention your name, describe a project, or share a preference, Iris remembers -without you having to ask. The [two-layer context system](/core-concepts/memory-system) balances completeness with efficiency: * **[Truths](/core-concepts/truths)**: Stable, core facts earned through behavioral evidence - always considered, ranked by relevance * **Memories**: Contextually relevant information retrieved based on what you're currently discussing ### Threaded Conversations Organize your conversations into [threads](/core-concepts/threads) — isolated contexts with independent history, summaries, and settings. Pin important threads, let Iris auto-name them, and [pin skills](/tools/agent-skills#pinning-skills-to-threads) to tailor each thread's expertise. ### Conversation Summarization Long conversations are automatically [summarized](/core-concepts/summarization) per thread to preserve context. These aren't simple excerpts — summaries capture key discussion points, emotional dynamics, and unresolved threads, creating continuity within each thread. ### Tool Integration Iris interacts with external services through an [agentic tool system](/tools/overview): * **Google Calendar**: View, create, and manage events * **Image Generation**: Create images with OpenAI * **Web Search**: Search for current information * **Truth & Memory Management**: Store, search, and update truths and memories directly ## Architecture | Layer | Technology | |-------|------------| | Backend | Laravel 12, PHP 8.4 | | Database | PostgreSQL 16 with pgvector | | Frontend | React 19, TypeScript, Inertia.js | | Styling | Tailwind CSS v4 | | LLM | Prism PHP (Anthropic by default, [configurable](/getting-started/configuration#providers)) | | Embeddings | Prism PHP (OpenAI by default, [configurable](/getting-started/configuration#providers)) | | Real-time | Laravel Reverb (WebSockets) | | Queue | Laravel Horizon with Redis | ## Requirements | Requirement | Version | |-------------|---------| | PHP | 8.4+ | | Node.js | 20+ | | PostgreSQL | 16+ with pgvector | | Redis | 6+ | | Composer | 2.x | ### API Keys (Default Providers) * **Anthropic API Key**: Powers Claude for chat and background tasks * **OpenAI API Key**: For text embeddings and image generation > \[!TIP] > These are for the default provider configuration. If you switch providers (e.g., to Ollama for local inference), you only need API keys for the providers you're actually using. See [Local Setup](/advanced/local-setup) for running Iris without any external API keys. ## Navigating the Documentation If you're setting up Iris for the first time: 1. Follow the [Installation Guide](/getting-started/installation) 2. Review [Configuration](/getting-started/configuration) to understand your options If you want to understand how Iris works: 1. Start with [Chat Interface](/core-concepts/chat-interface) for the request flow 2. Learn about [Threads](/core-concepts/threads) for conversation organization 3. Explore [Memory System](/core-concepts/memory-system) for the persistence layer 4. Understand [Truths](/core-concepts/truths) for stable, earned facts about you 5. Learn about [Tools](/tools/overview) for the agentic capabilities If you want to customize Iris: 1. Read [System Prompts](/core-concepts/system-prompts) to modify the AI persona 2. See [Custom Tools](/tools/custom-tools) to add new capabilities 3. Check [Customization](/advanced/customization) for the complete guide --- --- url: /license.md --- # Iris License Agreement Copyright (c) 2026-present TJ Miller. All rights reserved. ## Acceptance By accessing or using the Software, you agree to be bound by the terms of this License Agreement. If you do not agree to these terms, you may not access or use the Software. ## Definitions * **"Licensee"** means the individual who has purchased a license through GitHub Sponsorship or other authorized means. * **"Software"** means the Iris source code, documentation, and associated files made available to the Licensee. * **"Internal Use"** means use by the Licensee or the Licensee's employees within their own organization, where the Software is not made available to third parties. * **"End User"** means any person who is not the Licensee or an employee of the Licensee's organization. ## License Grant Subject to the terms of this Agreement, TJ Miller grants the Licensee a non-exclusive, non-transferable, non-sublicensable license to: 1. **Personal Use** — Install and run the Software for the Licensee's own personal use 2. **Internal Use** — Deploy the Software for use by employees within the Licensee's organization 3. **Modify** — Make changes to the Software for permitted uses 4. **Learn** — Study the Software's architecture and patterns for educational purposes ## Restrictions The Licensee may NOT: 1. **Customer-Facing Use** — Use the Software, or any portion thereof, in any product, application, or service where End Users interact with or benefit from the Software's functionality. This includes embedding, integrating, or exposing the Software's features to your customers, clients, or users. 2. **Commercial Distribution** — Offer the Software as a hosted service, SaaS platform, or managed solution to third parties, whether for payment or for free. 3. **Redistribute** — Share, publish, sublicense, sell, lease, or otherwise distribute the Software or any modified version to any third party. 4. **Repackage** — Include the Software or any portion thereof in any product, template, starter kit, theme, or distribution intended for use by others. 5. **Competing Products** — Use the Software to create any product or service that competes with Iris, regardless of whether it is offered commercially. 6. **Share Access** — Share your GitHub access, account credentials, downloaded source code, or any copies of the Software with any individual who has not purchased their own license. 7. **Remove Notices** — Remove, alter, or obscure any copyright notices, license terms, or attributions included in the Software. ## What Is Allowed * Running Iris as your personal AI assistant * Deploying Iris for internal use within your company (employees only) * Modifying Iris to customize the persona, add internal tools, or integrate with internal systems * Studying the codebase to learn AI application architecture and patterns * Using knowledge gained from Iris to inform your own original work ## What Is NOT Allowed * Offering Iris (or a modified version) as a service to customers or clients * Building a product where your users interact with Iris's AI assistant functionality * Embedding Iris into a commercial application as a feature for end users * Creating a SaaS, hosted service, or managed offering based on Iris * Selling, distributing, or sharing the source code * Uploading Iris to public repositories, package registries, or marketplaces * Sharing your copy with developers who haven't purchased their own license ## Per-Seat Licensing This is a **per-seat license**. Each individual who accesses the Software source code must hold their own valid license. If your team requires access, each team member must purchase a separate license through GitHub Sponsorship. Contractors working on your behalf may access the Software under your license only while actively engaged on your project, provided they do not retain copies after the engagement ends. ## Updates and Access Your license includes access to Software updates for as long as your GitHub Sponsorship remains active. If your sponsorship ends: * You may continue using the version of the Software you have already obtained * You will no longer have access to the repository or future updates * All other terms of this license continue to apply to your retained copy ## Ownership This license does not transfer any ownership or intellectual property rights. The Software, including all modifications and derivative works, remains the exclusive property of TJ Miller. This license does not grant any rights to use TJ Miller's name, trademarks, service marks, or logos. ## Patent Rights TJ Miller grants the Licensee a license under any patent claims TJ Miller can license that are necessarily infringed by the Software, to use the Software for permitted purposes only. This patent license does not cover any patent claims that become infringed by modifications you make to the Software. ## Termination This license terminates immediately if the Licensee breaches any of its terms. Upon termination: * All rights granted under this license cease immediately * The Licensee must destroy all copies of the Software in their possession * The Licensee must remove any deployed instances of the Software * TJ Miller may revoke repository access without notice TJ Miller reserves the right to terminate this license and revoke access for any violation, with or without prior notice. ## Enforcement If the Licensee is notified of a license violation, the Licensee has 30 days to cure the violation and come into full compliance. If the violation is not cured within 30 days, the license terminates permanently. For willful or egregious violations (such as intentional redistribution or operating a competing service), TJ Miller may terminate the license immediately without a cure period. ## No Warranty THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. TJ MILLER DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED. ## Limitation of Liability IN NO EVENT SHALL TJ MILLER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. TJ MILLER'S TOTAL LIABILITY SHALL NOT EXCEED THE AMOUNT PAID BY THE LICENSEE FOR THE LICENSE. ## Governing Law This Agreement shall be governed by and construed in accordance with the laws of the State of Michigan, United States, without regard to conflict of law provisions. ## Severability If any provision of this Agreement is held to be unenforceable or invalid, that provision shall be modified to the minimum extent necessary to make it enforceable, and all other provisions shall remain in full force and effect. ## Entire Agreement This Agreement constitutes the entire agreement between the parties concerning the Software and supersedes all prior or contemporaneous agreements, representations, warranties, and understandings. ## Contact For licensing questions or to report violations: tj@prismphp.com *** **By using Iris, you acknowledge that you have read, understood, and agree to be bound by this License Agreement.** --- --- url: /core-concepts/memory-consolidation.md --- # Memory Consolidation Over time, extraction creates many related memories about similar topics. Consolidation merges semantically similar memories into denser, more useful representations. ## Why Consolidation Matters * **Reduces redundancy** - Multiple similar memories merged into one * **Improves coherence** - Related facts combined into comprehensive memories * **Increases quality** - LLM review improves wording and accuracy ## How It Works Consolidation uses a two-phase job architecture for efficient parallel processing: **Phase 1: Cluster Building** 1. Find memories with high similarity (≥0.80) 2. Group them into clusters using greedy clustering 3. Dispatch one job per cluster **Phase 2: Cluster Processing** 1. Each cluster job asks the LLM if memories should merge 2. If approved, create consolidated memory and soft-delete originals 3. If rejected, memories remain separate The LLM may keep memories separate if they contain contradictory information, represent different time periods, or merging would lose important nuance. ## Example **Input memories:** * "Has 8 years of PHP experience" * "Works primarily with Laravel framework" * "Experienced with PHP development" **Result:** * "Experienced PHP developer with 8+ years of experience, primarily working with the Laravel framework" ## Generation Tracking Consolidation tracks how many times a memory has been merged through **generation tracking**. This enables organic memory evolution while preventing runaway over-consolidation. ### How Generations Work ``` Gen 0 ─┬─► Gen 1 ─┬─► Gen 2 ─┬─► Gen 3 ─┬─► Gen 4 ─┬─► Gen 5 (max) │ │ │ │ │ Original First Re-consolidation continues... memories merge ``` | Generation | Description | |------------|-------------| | **Gen 0** | Original memories from extraction or manual creation | | **Gen 1** | First consolidation (merging original memories) | | **Gen 2-4** | Re-consolidation of already-consolidated memories | | **Gen 5** | Maximum - these memories won't be re-consolidated | When memories are consolidated, the new memory's generation is calculated as `max(source generations) + 1`. ### Re-Consolidation Unlike many memory systems that only consolidate original memories, Iris can re-consolidate already-merged memories. This allows memories to evolve naturally over time as more related information is gathered. For example, a Gen 1 memory about "prefers TypeScript" might later merge with another Gen 1 memory about "values static typing" to form a richer Gen 2 memory. > \[!IMPORTANT] > The LLM applies extra scrutiny when re-consolidating. High-generation memories already pack dense information, so only truly redundant memories are merged. ### Generation Limits Generation 5 is the hard limit. Memories reaching this generation are excluded from future consolidation to prevent: * Over-abstraction losing important details * Runaway consolidation chains * Memory content becoming too generic ## Running Consolidation By default, consolidation dispatches jobs to the queue for parallel processing: ```bash # Queue batch for all users (default behavior) php artisan iris:consolidate-memories # Full sweep - process ALL memories, ignore days filter php artisan iris:consolidate-memories --full # Run synchronously (useful for debugging) php artisan iris:consolidate-memories --sync # Preview without changes (forces sync) php artisan iris:consolidate-memories --dry-run # Single user php artisan iris:consolidate-memories --user=1 # Custom threshold for full sweep php artisan iris:consolidate-memories --full --threshold=0.75 ``` ## Monitoring Health Check the health of your memory consolidation system: ```bash # View consolidation statistics php artisan iris:memory-health # Filter by specific user php artisan iris:memory-health --user=1 ``` The command shows: * **Generation distribution** - Count of memories at each generation * **Original vs consolidated** - Balance between new and merged memories * **Consolidation ratio** - Percentage of memories that are consolidation results * **At max generation** - Memories that won't be re-consolidated * **Eligible count** - Memories available for future consolidation **Example output:** ``` Memory Consolidation Health Generation Count Gen 0 (original) ............................................... 657 Gen 1 ............................................................ 444 Total memories ................................................. 1101 Original memories ............................................... 657 Consolidation results ........................................... 444 At max generation ................................................. 0 Consolidation ratio ........................................... 40.3% Eligible for consolidation ..................................... 1101 ``` ### Processing Batches When running in queue mode, the command returns a batch ID: ```bash php artisan iris:consolidate-memories # Dispatched batch: 9c3b5f2a-... # Process jobs (Horizon should already be running) php artisan horizon # Retry failed jobs in a batch php artisan queue:retry-batch 9c3b5f2a-... ``` Use Laravel Horizon for monitoring batch progress in a UI. ### Scheduled Runs Iris automatically schedules consolidation in `bootstrap/app.php`: ```php // Daily incremental at 3:00 AM $schedule->command('iris:consolidate-memories') ->dailyAt('03:00') ->withoutOverlapping() ->runInBackground(); // Weekly full sweep on Sundays at 4:00 AM $schedule->command('iris:consolidate-memories --full') ->weeklyOn(Schedule::SUNDAY, '04:00') ->withoutOverlapping() ->runInBackground(); ``` ## Job Architecture Consolidation uses two job types for efficient processing: ### ConsolidateUserMemories Handles Phase 1 for a single user: * Builds memory clusters (fast, no LLM calls) * Dispatches `ConsolidateMemoryCluster` jobs for each cluster * 60 second timeout ### ConsolidateMemoryCluster Handles Phase 2 for a single cluster: * Reconstructs cluster from memory IDs * Calls LLM to review and decide on merging * Executes consolidation if approved * 120 second timeout * Rate-limited to prevent API overload This architecture prevents timeout issues with large memory sets by processing clusters independently. ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `consolidation.similarity_threshold` | 0.80 | Minimum similarity to cluster | | `consolidation.days_lookback` | 3 | Days of memories to consider for daily runs | | `consolidation.max_cluster_size` | 5 | Max memories per cluster | | `consolidation.min_cluster_size` | 2 | Min memories to form a cluster | | `consolidation.max_generation` | 5 | Maximum consolidation generation | | `consolidation.jobs_per_minute` | 10 | Rate limit for queued jobs | Higher threshold (0.85+) is more conservative. Lower (0.75) merges more aggressively but may lose nuance. > \[!TIP] > Use `--full` for a weekly sweep to catch memories that didn't cluster during daily runs due to the time filter. ## Rate Limiting Consolidation jobs are rate-limited to prevent overwhelming LLM APIs: 1. **Preventive**: Jobs throttled to `jobs_per_minute` limit 2. **Reactive**: If rate limited by the API, jobs automatically retry after the limit resets using Prism's `resetsAt` timing 3. **Time-based retries**: Jobs use `retryUntil` with a 2-hour window, allowing unlimited rate-limit releases without failing Adjust `consolidation.jobs_per_minute` in `config/iris-custom.php` based on your API tier. The default of 10 jobs/minute leaves headroom for chat usage while consolidation runs. > \[!NOTE] > Only the `ConsolidateMemoryCluster` jobs are rate-limited since they make LLM calls. The parent `ConsolidateUserMemories` jobs run without throttling. --- --- url: /core-concepts/memory-extraction.md --- # Memory Extraction Memory extraction automatically identifies and stores important information from your conversations. Rather than requiring you to explicitly tell Iris what to remember, it analyzes your interactions and captures facts worth preserving. ## How It Works Extraction runs as a background job after every N messages (default: 10): 1. **Gather context**: The job collects up to 50 recent messages, including both user and assistant turns 2. **Analyze for memorable content**: An LLM reviews the conversation and identifies facts worth preserving 3. **Check for duplicates**: Existing memories are compared to avoid storing redundant information 4. **Create memories**: Each memory is stored with content, type, category, tags, and vector embedding The extraction prompt instructs the model to be selective -quality over quantity. Each memory should be self-contained and useful on its own, not dependent on conversation context. > \[!NOTE] > Extraction runs via the queue worker. Make sure `php artisan horizon` is running. ## What Gets Extracted ### Good Candidates | Type | Examples | |------|----------| | **Personal details** | Name, location, family members, birthday | | **Preferences** | Likes dark mode, prefers morning meetings, vegetarian | | **Goals** | Wants to learn piano, training for a marathon | | **Skills** | Experienced PHP developer, fluent in Spanish | | **Relationships** | Sarah is their manager, works with John on projects | | **Events** | Starting a new job next month, vacation planned for July | | **Context** | Works remotely, has a 2-hour commute | ### What Gets Skipped * **Transient context**: "I'm looking at the code right now" - not useful long-term * **Already known**: If a memory already exists, don't duplicate it * **Trivial details**: "I had coffee this morning" - unless there's a pattern * **Conversation mechanics**: "Thanks for your help" - not about the user ## Quality vs Quantity Tradeoff The `max_memories` setting (default: 6) intentionally limits how many memories can be created per extraction. This forces selectivity -the model must choose the most valuable facts to preserve. Increasing this limit captures more information but may dilute quality. The model might start storing borderline-useful facts that clutter memory retrieval later. > \[!TIP] > If important information seems to be missed, first check if extraction is running (Horizon dashboard at `/horizon`). If it is, consider lowering `extraction.threshold` to run more frequently rather than raising `max_memories`. ## Examples ### Example 1: Professional Context **Conversation:** > "I'm working on a Laravel project for a healthcare startup. We're building a patient portal that needs to be HIPAA compliant. I've been doing PHP for about 8 years now." **Extracted memories:** * "Works at a healthcare startup building a patient portal" (type: `fact`, category: `professional`) * "Has 8 years of PHP development experience" (type: `skill`, category: `professional`) * "Working on HIPAA-compliant software" (type: `context`, category: `professional`) ### Example 2: Personal Preferences **Conversation:** > "I really prefer having my meetings in the morning when I'm fresh. After lunch I'm usually in deep focus mode and don't want to be interrupted." **Extracted memories:** * "Prefers meetings in the morning when they feel fresh" (type: `preference`, category: `preferences`) * "Reserves afternoons for deep focus work" (type: `habit`, category: `professional`) ### Example 3: Relationship Information **Conversation:** > "My manager Sarah wants me to lead the API redesign project. I'll be working with the backend team on this." **Extracted memories:** * "Sarah is their manager" (type: `relationship`, category: `professional`) * "Leading the API redesign project" (type: `goal`, category: `professional`) ## Preventing Duplicates Before storing a new memory, extraction checks for semantic similarity with existing memories. If a very similar memory already exists, the new one is skipped or the existing one is updated. This prevents accumulation of near-duplicate memories like: * "Has 8 years of PHP experience" * "Experienced PHP developer for 8 years" * "Been doing PHP development for about 8 years" The duplicate detection uses the same embedding comparison as memory retrieval, with a high similarity threshold to catch only true duplicates. ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `extraction.threshold` | 10 | Messages between extractions | | `extraction.max_memories` | 6 | Maximum memories per extraction | | `extraction.timeout` | 120 | API timeout in seconds | | `extraction.model` | claude-sonnet-4-5 | Model for analysis | > \[!WARNING] > Higher `max_memories` limits may lead to lower-quality memories. The default encourages selectivity. ## Monitoring Extraction To see if extraction is working: 1. **Check the queue**: The Horizon dashboard at `/horizon` should show `ExtractMemories` jobs processing 2. **View memories**: The Memories page shows when memories were created 3. **Check logs**: Failed extractions log errors to `storage/logs/laravel.log` --- --- url: /core-concepts/memory-system.md --- # Memory System The memory system enables persistent, contextual conversations by storing and retrieving relevant information about your interactions. Unlike traditional chat applications that forget everything between sessions, Iris builds a growing understanding of you over time. ## Two Layers of Context Iris uses two complementary systems for remembering what matters: 1. **[Truths](/core-concepts/truths)** - Stable, core facts that are relevant across conversations. Your name, key relationships, and fundamental preferences. Truths are earned through behavioral evidence - memories that consistently prove useful get promoted automatically. 2. **Memories** - Contextual information retrieved based on what you're currently discussing. Memories about cooking surface when you're planning dinner, not when you're debugging code. This separation ensures Iris always knows the essentials while keeping context focused on what's relevant to the current conversation. > \[!NOTE] > Memories and Truths are **stored globally** across all [threads](/core-concepts/threads) — your preferences, relationships, and core facts are searchable regardless of which thread you're in. However, memory *recall* is thread-scoped: search queries are generated from the current thread's conversation, so the memories that surface are relevant to what you're discussing right now, not influenced by other threads. Thread-specific context (conversation history, summaries) is handled by the [thread system](/core-concepts/threads). ## Memory Retrieval When you send a message, Iris retrieves relevant context through semantic search: 1. **Query Generation**: An LLM analyzes recent conversation turns from the current thread and generates search queries 2. **Vector Search**: Queries are embedded and compared against your full memory pool using cosine similarity 3. **Ranking**: Results are scored and ranked by the composite scoring formula 4. **Filtering**: Only memories above the similarity threshold (default: 0.38) are included Query generation is thread-scoped — only conversation turns from the active thread inform what to search for. The search itself runs against all your memories globally. This means if you're discussing a recipe in one thread, cooking memories surface there without being influenced by a debugging conversation in another thread. ## Memory Types | Type | Description | |------|-------------| | `fact` | Objective information about the user | | `preference` | Likes, dislikes, and preferences | | `goal` | Things the user wants to achieve | | `event` | Past or upcoming events | | `skill` | Capabilities and expertise | | `relationship` | People and relationships | | `habit` | Regular behaviors and routines | | `context` | Situational information | ## Scoring Memories are ranked by a composite score that balances multiple factors: ``` score = (semantic × 0.60) + (recency × 0.25) + (frequency × 0.15) ``` ### Score Components | Component | Weight | What it measures | |-----------|--------|------------------| | **Semantic similarity** | 60% | How closely the memory relates to the current conversation, measured by cosine similarity between embeddings | | **Recency** | 25% | How recently the memory was created or accessed, decaying linearly over 90 days | | **Frequency** | 15% | How often the memory has been retrieved, indicating ongoing relevance | ### Type Bonuses Certain memory types receive score bonuses because they tend to be more relevant: * **Relationships**: +0.10 (people you know are often contextually important) * **Preferences and goals**: +0.05 (these shape how Iris should respond) * **Recent events**: +0.10 (upcoming or recent events are likely relevant) ### Example Scoring Consider a memory: "Prefers morning meetings before 10am" created 30 days ago, accessed 5 times. If you ask "When should we schedule our call?": * Semantic similarity might be 0.85 (highly relevant) * Recency: ~0.67 (30/90 days decay) * Frequency: ~0.50 (normalized) Raw score: `(0.85 × 0.60) + (0.67 × 0.25) + (0.50 × 0.15) = 0.75` With the preference bonus (+0.05): **0.80** ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `memory.max_results` | 7 | Maximum memories to include from semantic search | | `memory.similarity_threshold` | 0.38 | Minimum similarity score to include a memory | > \[!TIP] > If memories seem stale or irrelevant, adjust the scoring weights in config. Increase `recency` to prioritize recent memories, or `semantic` to prioritize relevance. ## How Memories Are Created 1. **[Automatic Extraction](/core-concepts/memory-extraction)** - Background job analyzes conversations and extracts memorable facts 2. **[Consolidation](/core-concepts/memory-consolidation)** - Similar memories are merged nightly, with [generation tracking](/core-concepts/memory-consolidation#generation-tracking) to enable organic memory evolution 3. **[Promotion to Truths](/core-concepts/truths)** - Frequently accessed memories can be promoted to Truths through distillation 4. **Manual Tools** - Iris can store, update, or delete memories directly during conversation ## Viewing Memories The Memories page in the UI shows all your memories with filtering and search. Consolidated memories display their generation level (e.g., "Merged Gen 2") to show how many times they've been consolidated. The Insights page includes consolidation statistics in the Memory Insights panel. ## Troubleshooting Memory Retrieval ### Memories not surfacing If relevant memories aren't appearing in conversations: 1. **Check the similarity threshold** - The default (0.38) is fairly permissive. If memories still aren't surfacing, the search queries may not match well. Try rephrasing your question to use similar language to how the memory is stored. 2. **Verify memories exist** - Check the Memories page to confirm the information was actually stored. 3. **Check if it should be a Truth** - If a memory is important enough to surface in most conversations, it might be better suited as a [Truth](/core-concepts/truths). Create it manually or wait for distillation to promote it. ### Too many irrelevant memories If Iris seems to include memories that aren't helpful: 1. **Raise the similarity threshold** - Increase `memory.similarity_threshold` in config to be more selective. 2. **Reduce max results** - Lower `memory.max_results` to include fewer total memories. ### Stale memories dominating If old memories keep appearing over newer, more relevant ones: 1. **Increase recency weight** - The default recency weight (0.15) is relatively low. Increasing it prioritizes recent memories. 2. **Let consolidation run** - Consolidation merges similar memories, which can help surface the most current version of information. 3. **Update outdated memories** - Use the update\_memory tool to correct information rather than creating new memories. --- --- url: /core-concepts/proactive-messages.md --- # Proactive Messages Iris can proactively reach out to you when she has something helpful to share. Instead of waiting for you to start a conversation, Iris monitors context like your calendar, recent conversations, and preferences to decide when a check-in would be valuable. > \[!TIP] > Proactive messages are **enabled by default**. You can adjust settings and boundaries in Settings > Proactive Messages. ## How It Works Iris runs a "heartbeat" every 30 minutes. During each heartbeat, she: 1. **Gathers context**: Thread briefs and latest summaries from active threads, calendar events, weather, memories, thread metadata, and your guidance 2. **Makes a decision**: Should she reach out, or give you space? 3. **Crafts a message**: If reaching out, she composes a personalized message 4. **Delivers it**: The message appears in its own dedicated [thread](/core-concepts/threads), with a notification if your tab is backgrounded. If you've connected [Telegram](/integrations/telegram-notifications), it also pushes to your phone The key insight: Iris decides whether to reach out, but you set the guardrails. It's not a rule engine - it's a relationship where you provide guidance and Iris interprets it with judgment. The heartbeat uses a [dedicated prompt stack](/core-concepts/system-prompts#heartbeat-prompt-stack) that's lighter than the full conversation prompt stack — it includes the persona, semantic memory recall, active thread context with summaries, calendar, weather, and current time, but omits conversation-specific prompts like pinned skills, autonomous execution guidance, and thread-scoped summaries. ## Setting Up Proactive Messages ### 1. Review Settings Proactive messages are enabled by default. Visit **Settings > Proactive Messages** to review or adjust your preferences. ### 2. Add Guidance Navigate to **Guidance** (in the sidebar) and add guidance entries that tell Iris when and how you'd like her to reach out. These aren't rigid rules - they're preferences Iris interprets with judgment. **Example guidance:** | Title | Description | |-------|-------------| | Morning check-ins | I like when you check in around 7-8am while I'm having coffee, especially if I have a busy day ahead | | Therapy follow-ups | Please check in about an hour after my therapy appointments to see how I'm doing | | Don't over-message | If we just talked within the last 2 hours, I probably don't need another check-in | | Calendar reminders | Give me a heads up 15-30 minutes before important appointments | ### 3. Set Boundaries (Optional) Boundaries are hard constraints Iris must respect - unlike guidance, these aren't suggestions. **Quiet Hours**: Block messages during specific time windows (e.g., 10pm - 7am) **Do Not Disturb**: Block all proactive messages until you manually disable it **Temporary Silence**: Quick buttons to silence for 1 hour, 4 hours, or until tomorrow ## Message Priority When Iris decides to reach out, she assigns a priority level: | Priority | Use Case | Example | |----------|----------|---------| | **High** | Time-sensitive situations | "Your meeting with Sarah starts in 15 minutes" | | **Normal** | Routine check-ins | "How are you feeling after your therapy session?" | | **Low** | Non-urgent thoughts | "When you have a moment, I noticed something about your schedule" | The priority helps you understand urgency at a glance. ## When Iris Gives Space Iris doesn't message just because she can. She'll skip a heartbeat when: * You just had a satisfying conversation recently * It's work hours with no urgent needs * There's no clear reason to interrupt * You're in an active conversation (message sent in the last 5 minutes) * A boundary is active (quiet hours, DND, or temporary silence) Every decision is logged with Iris's reasoning, so you can see why she reached out or chose not to. ## Viewing Activity Logs Navigate to **Logs** (in the sidebar) to see Iris's heartbeat history. Each entry shows: * **Action**: Whether Iris reached out or gave space * **Reasoning**: Why she made that decision * **Context snapshot**: What information she considered * **Conversation link**: If she sent a message, a link to that conversation Use this to understand Iris's decision-making and tune your guidance accordingly. ## Scheduled Follow-ups Iris can schedule her own future check-ins. For example, if you mention "I have a big presentation tomorrow," Iris might: 1. Acknowledge it now 2. Schedule a follow-up for tomorrow afternoon to ask how it went You'll see scheduled follow-ups in the Logs page. ## Configuration Proactive message settings are in `config/iris.php`: | Setting | Default | Description | |---------|---------|-------------| | `heartbeat.model` | `claude-sonnet-4-5` | Model for heartbeat decisions | | `heartbeat.max_steps` | `30` | Tool iterations when crafting messages | | `heartbeat.history_limit` | `null` | Messages included (null = use default) | | `heartbeat.context_max_threads` | `3` | Most recently active threads to include in heartbeat context | | `heartbeat.prompts` | *(see below)* | [Prompt stack](/core-concepts/system-prompts#heartbeat-prompt-stack) for heartbeat decisions and message crafting | The heartbeat uses a dedicated prompt stack configured separately from the main conversation prompts. This keeps the heartbeat focused on what it needs — persona, memory, thread context with summaries, calendar, weather, and time — without the overhead of conversation-scoped prompts like pinned skills or autonomous execution guidance. ### Customizing Behavior ```php // config/iris-custom.php return [ 'heartbeat' => [ 'max_steps' => 10, // Simpler message crafting 'context_max_threads' => 5, // More thread awareness ], ]; ``` ## How Decisions Are Made Each heartbeat follows this flow: ``` ┌─────────────────────────────────────────┐ │ Heartbeat Scheduled │ │ (every 30 minutes) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Check Eligibility │ │ - User has heartbeat enabled? │ │ - Any boundaries active? │ │ - Active conversation in progress? │ └─────────────────┬───────────────────────┘ │ eligible? │ ▼ ┌─────────────────────────────────────────┐ │ Gather Context │ │ - Thread briefs + latest summaries │ │ - Thread metadata & activity patterns │ │ - Calendar events (next 2 hours) │ │ - Weather conditions │ │ - User's guidance entries │ │ - Scheduled follow-ups │ │ - Memories and Truths │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Make Decision │ │ - Send message or give space? │ │ - What priority level? │ │ - Schedule a follow-up? │ └─────────────────┬───────────────────────┘ │ send message? │ ▼ ┌─────────────────────────────────────────┐ │ Craft Message │ │ - Full agent with tools │ │ - Can search memory, check calendar │ │ - Personalized, context-aware │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Deliver & Log │ │ - Save to dedicated proactive thread │ │ - Broadcast via WebSocket │ │ - Push to Telegram (if enabled) │ │ - Record decision in logs │ └─────────────────────────────────────────┘ ``` ## Running Manually Test heartbeat behavior with the Artisan command: ```bash # Check all eligible users (dry run) php artisan iris:heartbeat --dry-run # Process a specific user php artisan iris:heartbeat --user=1 # Force process (bypass boundary checks) php artisan iris:heartbeat --user=1 --force ``` ## Scheduling The heartbeat runs automatically via Laravel's scheduler. It's configured in `routes/console.php`: ```php Schedule::command('iris:heartbeat') ->everyThirtyMinutes() ->withoutOverlapping(); ``` ## Costs Each heartbeat consumes API tokens for: 1. **Decision phase**: Structured output to decide whether to reach out 2. **Message crafting**: Full agent response with tool access (only if sending a message) Token usage is tracked in the `heartbeat` source type. Start with conservative guidance that results in fewer messages while you learn Iris's decision-making patterns. ## Troubleshooting **Messages not appearing**: Verify proactive messages haven't been disabled in Settings > Proactive Messages and that the scheduler is running. Check the Logs page to see if heartbeats are being processed. **Too many messages**: Add guidance like "Don't over-message - give me space between check-ins" and review the Logs to see what's triggering outreach. **Messages at bad times**: First, verify your timezone is correct in Settings > Profile. Then set up Quiet Hours in Settings > Proactive Messages to block specific time windows. **"Blocked" status in logs**: Check your boundaries - you might have DND enabled or be within quiet hours. Use `--force` with the Artisan command to test bypassing boundaries. ## Telegram Push Notifications By default, proactive messages only appear in the web UI. If you'd like to receive them on your phone, you can connect Telegram as a push notification channel. Once set up, every proactive message also arrives as a Telegram notification with a one-tap link back to the full thread. See [Telegram Notifications](/integrations/telegram-notifications) for setup instructions. ## Disabling Proactive Messages Toggle off "Enable Proactive Messages" in Settings > Proactive Messages. Iris will stop running heartbeat checks and sending proactive messages. --- --- url: /advanced/local-setup.md --- # Running Iris Locally Iris supports configurable providers for every subsystem, which means you can run it entirely on local hardware using [Ollama](https://ollama.com) — no external API keys required. This is great for privacy-conscious setups, offline use, or just avoiding API costs during development. ## How It Works Every Iris subsystem that calls an LLM (chat, memory extraction, summarization, consolidation, embeddings, etc.) has its own `provider` and `model` settings. By default these point to Anthropic and OpenAI, but you can override any or all of them in `config/iris-custom.php` to use Ollama or any other [Prism provider](https://prismphp.com). ## Prerequisites * **Ollama installed and running** — [Install Ollama](https://ollama.com/download) for your platform * **A chat model** — You'll need a model that supports tool use and structured output, since Iris relies on both heavily * **An embedding model** — For semantic memory search * **Sufficient hardware** — Local models need memory. Check your model's requirements against your available GPU VRAM (or system RAM for CPU inference) ## Step 1: Pull Your Models You'll need a chat model and an embedding model. Pull whichever models you prefer from the [Ollama library](https://ollama.com/library): ```bash ollama pull ollama pull ``` The key requirements for the chat model are **tool/function calling support** and **structured output reliability** — Iris uses both extensively across all subsystems. Verify Ollama is running: ```bash ollama list ``` ## Step 2: Configure the Environment Ollama's default URL is `http://localhost:11434`. If you're running it elsewhere, set the URL in your `.env`: ```bash # .env (only needed if Ollama isn't on localhost:11434) OLLAMA_URL=http://localhost:11434 ``` Since you're not using Anthropic or OpenAI, you can remove or leave blank those API keys: ```bash # .env # ANTHROPIC_API_KEY= # Not needed for local-only setup # OPENAI_API_KEY= # Not needed for local-only setup ``` ## Step 3: Create Your Custom Config Create `config/iris-custom.php` to point every subsystem at Ollama. Here's a complete example using `qwen3.5:35b` for chat and `qwen3-embedding` for embeddings — swap in your preferred models: ```php [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Disable Anthropic-specific provider tools (web search/fetch) // These are Anthropic-only features and won't work with other providers 'provider_tools' => [], // Conversation summarization 'summarization' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Truth crystallization and promotion 'truths' => [ 'crystallization_provider' => 'ollama', 'crystallization_model' => 'qwen3.5:35b', 'promotion_provider' => 'ollama', 'promotion_model' => 'qwen3.5:35b', ], // Memory recall query generation 'memory' => [ 'recall_provider' => 'ollama', 'recall_model' => 'qwen3.5:35b', ], // Memory extraction from conversations 'extraction' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Memory consolidation 'consolidation' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Truth consolidation 'truth_consolidation' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Embeddings for semantic search 'embeddings' => [ 'provider' => 'ollama', 'model' => 'qwen3-embedding', 'provider_options' => [ 'dimensions' => 1536, ], ], // Proactive messages (heartbeat) 'heartbeat' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Sub-agent for task delegation 'subagent' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], ]; ``` ## Embedding Dimensions Iris uses pgvector with 1536-dimensional vectors by default (matching OpenAI's `text-embedding-3-small`). If your Ollama embedding model produces different dimensions, you have two options: 1. **Set `dimensions` in `provider_options`** to 1536 (shown above) — this works if the model supports dimension configuration 2. **Match the model's native dimensions** — this requires updating the database column size in a migration For most setups, setting `'dimensions' => 1536` in `provider_options` is the simplest path. > \[!WARNING] > If you switch embedding models or providers after memories have been stored, the existing embeddings won't be compatible with the new model. You'll need to re-embed existing memories or start fresh. ## Things to Know **Provider tools.** Anthropic's built-in web search and web fetch tools are provider-specific and won't work with other providers. The example config above disables them with `'provider_tools' => []`. **Structured output.** Iris's background tasks (extraction, consolidation, truth crystallization) rely heavily on structured output. Make sure your chosen model handles JSON schema responses reliably. **Tool use.** Iris relies on tools for agentic behavior. Make sure your chat model supports tool/function calling. ## Hybrid Configurations You don't have to go fully local. Since every subsystem has its own provider, you can mix cloud and local based on what matters most to you: ```php // config/iris-custom.php return [ // Use Claude for chat, Ollama for everything else 'agent' => [ 'provider' => 'anthropic', 'model' => 'claude-sonnet-4-5', ], // Background tasks run locally 'extraction' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], 'summarization' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], 'consolidation' => [ 'provider' => 'ollama', 'model' => 'qwen3.5:35b', ], // Local embeddings 'embeddings' => [ 'provider' => 'ollama', 'model' => 'qwen3-embedding', 'provider_options' => [ 'dimensions' => 1536, ], ], ]; ``` This gives you cloud-quality chat while offloading batch work to local models — a practical middle ground between quality and cost. --- --- url: /tools/shell-commands.md --- # Shell Commands Iris can execute shell commands on your server, enabling file operations, system queries, running scripts, and other CLI tasks. This turns Iris into a capable assistant for development workflows and system administration. > \[!WARNING] > Shell command execution is **disabled by default**. Enable it only on trusted deployments where you understand the security implications. ## What You Can Do With shell commands enabled, Iris can help with: * **File operations**: Reading, writing, listing, moving files * **Running scripts**: Build tools, test suites, deployment scripts * **System diagnostics**: Disk usage, process monitoring, logs * **Development tasks**: Git operations, package management, database queries * **Project management**: Creating directories, scaffolding, file organization ## Setup Enable shell commands in your `.env`: ```bash IRIS_SHELL_ENABLED=true ``` That's it - Iris can now execute commands during conversations. ## Example Conversations ### File Operations > **You**: "What's in my Downloads folder?" > > **Iris**: *\[executes `ls -la ~/Downloads`]* > > "You have 23 files in Downloads. The largest ones are: project-backup.zip (2.1GB), video-recording.mp4 (890MB)..." ### Git Workflow > **You**: "What's the git status of my project at ~/code/myapp?" > > **Iris**: *\[executes `git status` in that directory]* > > "You have 3 modified files and 1 untracked file. The modified files are: app/Models/User.php, config/app.php, and routes/web.php. Want me to show you the diff?" ### Running Commands > **You**: "Run the test suite for my Laravel project" > > **Iris**: *\[executes `php artisan test`]* > > "All 47 tests passed. 2 tests were skipped (marked as incomplete). The suite took 4.2 seconds." ### Multi-Step Tasks > **You**: "Create a project structure with src, tests, and config directories, then initialize git" > > **Iris**: *\[executes multiple commands]* > > "Done! I created the directories and initialized a git repo. Here's what I did: > > * Created project/src, project/tests, project/config > * Ran git init > * The repo is ready for your first commit." ## How Iris Handles Errors Iris doesn't give up when something fails. It reads the error, diagnoses the issue, and tries to fix it: > **You**: "Install the spatie/laravel-permission package" > > **Iris**: *\[runs `composer require spatie/laravel-permission`]* > > *\[command fails - PHP version mismatch]* > > **Iris**: "The latest version requires PHP 8.2, but you're on 8.1. Let me install a compatible version..." > > *\[runs `composer require spatie/laravel-permission:^5.0`]* > > "Installed spatie/laravel-permission v5.11. This version is compatible with your PHP 8.1 setup." ## Security Shell commands include multiple safety layers to prevent dangerous operations. ### Blocked Commands Privilege escalation tools are always blocked: * `sudo`, `su`, `doas`, `pkexec` Attempting these returns an error explaining why. ### Blocked Patterns Dangerous command patterns are detected and rejected: * Recursive force deletion of root (`rm -rf /`) * Direct writes to disk devices (`> /dev/sda`) * Filesystem formatting (`mkfs`) * Raw disk writes (`dd of=/dev/...`) ### Clean Environment Commands run with only essential environment variables: | Variable | Purpose | |----------|---------| | `PATH` | System path | | `HOME` | User home directory | | `USER` | Current user | | `SHELL` | User's shell | | `TERM` | Terminal type | | `LANG`, `LC_ALL` | Locale settings | Sensitive variables (API keys, tokens, credentials) are not inherited. ### Resource Limits * **Timeout**: Commands timeout after the specified duration (default 30s, max 300s) * **Output size**: Controlled by `shell.max_output_length` (default 50,000 characters). When output exceeds this limit, Iris shows the **tail** of the output — the last N characters — because shell command endings typically contain the most relevant information: error messages, final results, exit summaries. The beginning of long output (verbose startup logs, progress bars) is less useful to the model. When truncation occurs, a notice is prepended to the output: ``` ... [OUTPUT TRUNCATED — showing last 50000 of 120000 characters] ``` The full output is preserved in the database; truncation only affects what's sent to the LLM's context window. ## Configuration Customize shell behavior in `config/iris.php`: ```php 'shell' => [ 'enabled' => env('IRIS_SHELL_ENABLED', false), 'default_timeout' => 30, 'max_timeout' => 300, 'max_output_length' => 50000, 'default_working_directory' => null, 'blocked_executables' => ['sudo', 'su', 'doas', 'pkexec'], 'blocked_patterns' => [ // Dangerous patterns... ], 'inherit_env_vars' => ['PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL'], ], ``` ### Setting a Default Directory If most of your work happens in one place: ```php // config/iris-custom.php return [ 'shell' => [ 'default_working_directory' => '/home/user/projects', ], ]; ``` ### Adding More Blocked Commands Block additional executables you don't want Iris to use: ```php // config/iris-custom.php return [ 'shell' => [ 'blocked_executables' => [ 'sudo', 'su', 'doas', 'pkexec', // Keep the defaults 'shutdown', 'reboot', 'halt', // Add more ], ], ]; ``` ## Tips for Effective Use **Be specific about locations**: "List files in /var/log" works better than "show me log files" **Include project context**: "In my Rails project at ~/code/myapp, run the test suite" **Let Iris iterate**: If something fails, Iris will try alternative approaches before asking for help **Verify critical operations**: For important file changes, ask Iris to show you what changed ## When Iris Asks for Help Most errors are handled autonomously, but some require your input: **Genuine blockers:** * Ambiguous requirements ("Which config format?") * Destructive operations without explicit permission * Missing credentials or authentication * Fundamental approach decisions **Not blockers** (Iris handles these): * Command syntax errors * Missing dependencies * Wrong file paths * Permission issues with workarounds ## Troubleshooting **Command blocked**: If a command is blocked, Iris will explain why. You may need to run it manually outside of Iris. **Timeout errors**: For long-running commands, ask Iris to increase the timeout: "Run the full test suite with a 5 minute timeout" **Permission denied**: Iris can't use sudo. If elevated permissions are needed, you'll need to run that command yourself. **Output truncated**: When output exceeds `shell.max_output_length` (default 50,000 characters), Iris shows the tail of the output. The truncation notice at the top of the response tells you how many characters were omitted. Ask Iris to filter or summarize if you need specific information from earlier in the output. ## Disabling Shell Commands To disable (the default), ensure your `.env` doesn't enable it: ```bash # .env IRIS_SHELL_ENABLED=false ``` Or disable the tool while keeping other tools: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\Shell\RunShellCommandTool::class, ], ]; ``` --- --- url: /core-concepts/summarization.md --- # Summarization As conversations grow longer, sending the entire history becomes impractical. Iris automatically condenses older messages into narrative summaries that preserve context, emotional dynamics, and unresolved threads. Summaries are scoped to individual [threads](/core-concepts/threads) — each thread maintains its own independent summary chain. ## Why Summarization Matters Without summarization, you'd face a tradeoff: * **Keep all messages**: Context window fills up, costs increase, responses slow down * **Drop old messages**: Lose important context, Iris forgets what you discussed Summarization offers a middle path: older messages are compressed into rich summaries that capture what matters, while recent messages stay in full detail. ## How It Works Summarization now triggers based on token budget usage rather than message count. Before each LLM request, Iris checks the `prompt_tokens` reported by the previous turn against the model's context window: ``` if prompt_tokens >= context_window × compaction_threshold → summarize ``` At the default `compaction_threshold` of `0.75`, summarization fires when the last request consumed 75% or more of the context window — before the window overflows, not after. The process: 1. **Check token usage**: Compare the most recent turn's `prompt_tokens` against `context_window × context.compaction_threshold` 2. **Trigger if above threshold**: If token usage exceeds the threshold, start summarization 3. **Select turns to summarize**: Take older turns from the thread, leaving the most recent `context.prune_protect_turns` turns in full detail 4. **Generate summary**: An LLM creates a structured summary capturing key information 5. **Mark messages as summarized**: Link the summarized messages to the new summary The protected tail (most recent turns) stays in full detail so Iris can reference recent exchanges naturally. Each thread tracks its own summarization state independently — a long-running work thread might have several summaries while a short thread about dinner plans has none. ## What Gets Captured Summaries aren't just text excerpts -they're structured documents that capture multiple dimensions of the conversation: ### Narrative Summary A 150-300 word narrative that tells the story of the conversation segment. This is what gets injected into the system prompt. **Example:** > "The user discussed their ongoing Laravel project, expressing frustration with performance issues in the API layer. We explored several optimization strategies including query caching and eager loading. The conversation shifted to their upcoming vacation plans, and they mentioned needing to hand off the project to a colleague named Marcus. The user seemed stressed about the timeline but optimistic about the technical solutions we discussed." ### Emotional Markers Key emotional moments with intensity scores (0.0-1.0): ```json [ {"moment": "Expressed frustration with API performance", "intensity": 0.7}, {"moment": "Relief when caching solution clicked", "intensity": 0.6}, {"moment": "Excitement about vacation plans", "intensity": 0.5} ] ``` ### Thread Tracking **Unresolved threads** - topics that came up but weren't concluded: * "Performance testing before handoff" * "Meeting with Marcus about the project" **Resolved threads** - topics that reached a conclusion: * "Caching strategy for API endpoints" * "Vacation dates confirmed" ### Relationship Dynamics How trust and rapport evolved during this segment: * Formality level changes * Building understanding * Areas of strong agreement or disagreement ### Key Facts Important information learned during this segment that might warrant memory extraction: * "Works with a colleague named Marcus" * "Has vacation planned soon" * "API performance is a current priority" ## Summary Chaining Within each thread, summaries form a chain, with each referencing its predecessor via `previous_summary_id`. This creates continuity within a thread's conversation history. ``` Thread: "Work Project" Summary 1 → Summary 2 → Summary 3 (most recent) ↑ ↑ ↑ Links to Links to Injected nothing #1 into context ``` Each summary includes a **narrative thread** — a bridging sentence that connects to the previous summary: > "Continuing from our discussion about the API refactoring project..." This helps Iris maintain conversational continuity within a thread even when the full history isn't available. Summary chains are completely independent between threads — a summary in one thread never references a summary from another. ## When Summaries Are Used Up to 3 recent summaries from the active thread are included in the system prompt for each request. They appear in the context after recalled memories, providing: * Historical context from earlier in the thread * Emotional continuity (Iris remembers how conversations felt) * Awareness of unresolved topics within the thread ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `context.compaction_threshold` | `0.75` | Token usage fraction (of context window) that triggers summarization | | `context.prune_protect_turns` | `2` | Recent turns kept in full detail during summarization | | `summarization.threshold` | `40` | Secondary guard: unsummarized message count that also triggers summarization | | `summarization.timeout` | `120` | API timeout in seconds for summary generation | | `summarization.model` | `claude-sonnet-4-5` | Model used to generate summaries | ### Understanding the Settings * **context.compaction\_threshold**: The primary trigger. When the previous turn's reported token usage reaches this fraction of the context window, summarization runs before the next request. * **context.prune\_protect\_turns**: The number of recent turns preserved verbatim during summarization. These turns are never included in a summary until a subsequent compaction cycle. * **summarization.threshold**: A secondary message-count guard. Summarization can also fire when unsummarized messages accumulate past this count, independent of token usage. > \[!WARNING] > Very aggressive summarization (low compaction threshold) may lose nuance from recent turns. The defaults balance context preservation with token efficiency. ## Viewing Summaries Summaries are stored in the `conversation_summaries` table, scoped to threads. You can explore them via: ```bash php artisan tinker >>> $thread = User::first()->threads()->first(); >>> $thread->conversationSummaries()->latest()->first() ``` Each summary includes all the structured fields (emotional markers, threads, etc.) as JSON columns. ## See Also * [Context Management](/core-concepts/context-management) — how the token budget is calculated, how history is loaded against that budget, how tool output pruning reclaims space, and the full compaction lifecycle including progressive truncation fallback --- --- url: /core-concepts/system-prompts.md --- # System Prompts The system prompt tells Iris who it is and what it knows about you. It's assembled dynamically for each request, combining a static persona with contextual information like memories, summaries, and calendar events. ## How It Works Every time you send a message, Iris builds a system prompt by rendering a series of prompt classes in order. Each class is responsible for one section of the prompt: ``` ┌─ Cached Group (1h TTL) ─────────────────────┐ │ IrisStaticPrompt ← Identity │ │ AutonomousExecutionPrompt ← Agent behavior │ │ SkillsPrompt ← Skills (BP #1) │ └──────────────────────────────────────────────┘ ┌─ Cached Group (ephemeral) ──────────────────┐ │ PinnedSkillsPrompt ← Pinned skills │ │ PinnedPromptsPrompt ← Pinned prompts │ │ SummaryPrompt ← Summaries │ │ CrossThreadContextPrompt ← Briefs (BP #2) │ └──────────────────────────────────────────────┘ MemoryPrompt ← Truths + memories CalendarPrompt ← Upcoming events WeatherPrompt ← Current conditions CurrentTimePrompt ← Current date/time ``` Prompts are organized into [cache breakpoint groups](/core-concepts/cache-breakpoints) to optimize token costs with Anthropic's prompt caching. Cached groups share a single cache breakpoint, while standalone prompts below the groups contain dynamic, per-request content that isn't cached. The result is a personalized, context-aware prompt that includes everything Iris needs to respond appropriately. The [heartbeat system](/core-concepts/proactive-messages) uses a separate, purpose-built prompt stack. See [Heartbeat Prompt Stack](#heartbeat-prompt-stack) for details. ## Architecture Prompts are **self-contained**: each prompt injects a `RequestContext` and any services it needs, then fetches its own context when `content()` is called. This provides: * **Uniform pattern**: Core and custom prompts work identically * **Testability**: Prompts can be tested in isolation * **Flexibility**: Each prompt injects only what it needs * **Conditional rendering**: Prompts can return empty content if they have nothing to contribute ## Prompt Pipeline Each prompt is a separate class rendered in order. The default pipeline organizes prompts into [cache breakpoint groups](/core-concepts/cache-breakpoints) — cached groups for stable content and standalone entries for dynamic content: ### Cached Group 1: Static Content (1h TTL) These three prompts form the first cache group. Their content rarely changes, so they share a single cache breakpoint with a 1-hour TTL. **Static Prompt** — Core identity and behavior: * Identity and personality * Communication style * Tool usage guidelines **Autonomous Execution Prompt** — Behavioral guidance for autonomous tool usage: * Iterate on errors instead of stopping after one failure * Verify results before claiming success * Execute multi-step plans without asking permission between steps * Clear escalation criteria for when to ask vs. keep going This applies to all tools — shell, filesystem, memory, calendar, etc. Tool-specific mechanics (path conventions, read-before-edit gates) live in the tool descriptions themselves. **Skills Prompt** — Lists available [agent skills](/tools/agent-skills) for Iris to activate: * Skill names and descriptions from `.agents/skills/` * Only renders when skills are enabled and available * Content is loaded from disk files, not generated per-request ### Cached Group 2: Pinned & Thread Context (Ephemeral) These prompts form the second cache group with the default 5-minute TTL. Their content changes more often than core identity — when you pin or unpin skills, when summaries generate, or when thread briefs update — so they use a shorter cache lifetime. **Pinned Skills Prompt** — Injects the **full content** of skills [pinned to the active thread](/tools/agent-skills#pinning-skills-to-threads). Unlike the Skills Prompt above (which lists skill names for on-demand activation), this prompt loads each pinned skill's complete instructions into the system prompt so they're always active. * Reads pinned skill names from the thread's settings * Loads each skill's full content via the skill loader * Each skill becomes a separate system message * Only renders when the active thread has pinned skills **Pinned Prompts Prompt** — Injects custom prompts pinned to the thread. Because pinned content is loaded in full, it consumes context window space on every message. The thread settings modal shows a context consumption notice so you can see how much space your pinned skills use. If a pinned skill has been removed from disk, Iris shows a warning in the thread settings modal. **Summary Prompt** — Injects recent conversation summaries for continuity: * Narrative arc from previous conversations * Emotional context and relationship dynamics * Only renders when summaries exist **Cross-Thread Context Prompt** — Injects [thread briefs](/core-concepts/thread-briefs) from other active threads: * Briefs from the most recently active threads (up to 5, configurable) * Current thread is always excluded * Static timestamps for cache efficiency * Only renders when other threads have briefs and `iris.briefs.enabled` is true > \[!TIP] > When no skills are pinned and no briefs or summaries exist, the prompts in this group return empty content. The group doesn't consume a cache breakpoint in that case — it's as if it doesn't exist. ### Memory Prompt Recalls relevant context for the current message: * [Truths](/core-concepts/truths): Stable, core facts ranked by relevance (pinned Truths always included) * Memories: Semantically similar memories found via search * Only renders when Truths or memories exist ### Calendar Prompt Retrieves upcoming calendar events for the current user: * Upcoming events for the next 7 days * Calendar names and default calendar info * Only renders when events exist ### Weather Prompt Provides current weather conditions for the user's configured location. Only renders when weather data is available. ### Current Time Prompt Provides the current date and time in the user's timezone (falling back to the configured default). Always renders. ## Heartbeat Prompt Stack The [heartbeat system](/core-concepts/proactive-messages) uses a dedicated prompt stack configured separately from the conversation prompts. The heartbeat needs Iris's personality for consistent voice, but doesn't need conversation-scoped prompts like pinned skills, autonomous execution guidance, or thread summaries. ``` ┌─ Cached Group (ephemeral) ──────────────────┐ │ PersonaPrompt ← Voice (BP #1) │ └──────────────────────────────────────────────┘ HeartbeatConversationContextPrompt ← Thread briefs + summaries MemoryPrompt ← Truths + memories CalendarPrompt ← Upcoming events WeatherPrompt ← Current conditions CurrentTimePrompt ← Current date/time ``` **PersonaPrompt** — The core Iris persona extracted as a standalone prompt. This gives the heartbeat a consistent voice when crafting proactive messages without the full `IrisStaticPrompt` (which includes tool invocation protocols and other conversation-specific guidance). **HeartbeatConversationContextPrompt** — Provides awareness of active conversations. For each of the user's most recently active threads (up to `heartbeat.context_max_threads`), it includes: * Thread name and last activity timestamp * Thread brief (if available) * Latest conversation summary with narrative thread, emotional state, unresolved threads, and active goals This gives the heartbeat enough context to decide whether to reach out based on what's happening across conversations — without the full conversation prompt stack. The heartbeat prompt stack is configured via `iris.heartbeat.prompts` and uses the same format as the main `iris.prompts` config (cache groups and standalone entries). The `SystemPromptBuilder` reads from this alternate config key when building heartbeat system messages. ## Prompt Templates Templates are Blade files in `resources/views/prompts/`: ``` prompts/ ├── personas/ │ └── iris-static.blade.php # Core identity + protocols ├── persona.blade.php # Standalone persona definition ├── recalled-context.blade.php # Memory context ├── calendar-context.blade.php # Calendar events ├── weather-context.blade.php # Weather conditions ├── summary-context.blade.php # Conversation summaries ├── cross-thread-context.blade.php # Thread briefs └── heartbeat-conversation-context.blade.php # Thread briefs + summaries (heartbeat) ``` ## Customizing Prompts Create your own prompt classes to customize Iris's behavior. Each prompt is self-contained and injects the dependencies it needs. The `content()` method can return either a string or a Blade view: ```php $this->weather->getForecast( $this->requestContext->user()?->id ), ]); } } ``` For simpler prompts, return a string directly: ```php requestContext->user(); if (! $user) { return ''; // No content for unauthenticated requests } // Customize based on user or message $message = $this->requestContext->message(); if (str_contains(strtolower($message), 'urgent')) { return "## Priority Mode\n\nThe user has indicated urgency."; } return ''; } ``` ### Registering Custom Prompts Add your prompt to `config/iris-custom.php`. The prompts array **replaces** the default list entirely, so include the core prompts and cache groups you want to keep: ```php // config/iris-custom.php return [ 'prompts' => [ // Cached group — static content with 1h TTL [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, ], ], // Cached group — pinned content, summaries, and cross-thread context [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CrossThreadContextPrompt::class, ], ], // Dynamic, per-request content (no caching) App\Prompts\MemoryPrompt::class, App\Extensions\Prompts\WeatherPrompt::class, // Your custom prompt App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` Dynamic custom prompts go as standalone entries after the cached groups. If your prompt is static content that rarely changes, consider adding it to an existing cached group instead. See [Cache Breakpoints](/core-concepts/cache-breakpoints) for more on grouping strategies. ## Prompt Ordering Considerations Prompts are rendered in order, and that order matters. The system prompt flows from cached groups of stable content to standalone dynamic entries: 1. **Cached Group 1 — Static identity and skills**: Establishes who Iris is, how it behaves, and what skills are available. Cached with a 1h TTL because this content rarely changes. 2. **Cached Group 2 — Pinned content, summaries, and cross-thread context**: Thread-specific skills and prompts, conversation summaries, and [thread briefs](/core-concepts/thread-briefs) from other active conversations. Cached with the default 5m TTL since this content can change mid-session but doesn't change per-request. 3. **Memories** (standalone): Facts about the user that inform the response — changes per request. 4. **Integrations** (standalone): Calendar, weather, or other external data — changes per request. 5. **Temporal context last** (standalone): Current date/time anchors everything — always changes. This ordering maximizes prompt cache hit rates. Because Anthropic's caching is prefix-based, stable content at the top gets cached while dynamic content at the bottom changes per-request without invalidating the cache. See [Cache Breakpoints](/core-concepts/cache-breakpoints) for the full details on how groups map to breakpoints. When adding custom prompts, consider where the information fits logically. Static content that rarely changes should go in a cached group (or be added to an existing one). Dynamic, per-request content like project status or user-specific context should be standalone entries, typically placed after memories but before temporal context. ## Complete Custom Prompt Example Here's a full example of creating and registering a custom prompt that injects work project context: ### 1. Create the Prompt Class ```php requestContext->user(); if (! $user) { return ''; } $projects = $this->projectService->getActiveProjects($user->id); if ($projects->isEmpty()) { return ''; } return view('prompts.extensions.work-context', [ 'projects' => $projects, ]); } } ``` ### 2. Create the Blade Template ```blade {{-- resources/views/prompts/extensions/work-context.blade.php --}} ## Current Work Projects The user is currently working on these projects: @foreach($projects as $project) - **{{ $project->name }}**: {{ $project->description }} - Status: {{ $project->status }} - Deadline: {{ $project->deadline?->format('F j, Y') ?? 'No deadline' }} @endforeach Use this context when the user asks about work or projects. ``` ### 3. Register the Prompt ```php // config/iris-custom.php return [ 'prompts' => [ [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Prompts\AutonomousExecutionPrompt::class, App\Prompts\SkillsPrompt::class, ], ], [ 'cache' => ['type' => 'ephemeral'], 'prompts' => [ App\Prompts\PinnedSkillsPrompt::class, App\Prompts\PinnedPromptsPrompt::class, App\Prompts\SummaryPrompt::class, App\Prompts\CrossThreadContextPrompt::class, ], ], App\Prompts\MemoryPrompt::class, App\Extensions\Prompts\WorkContextPrompt::class, // After memories App\Prompts\CalendarPrompt::class, App\Prompts\CurrentTimePrompt::class, ], ]; ``` The `WorkContextPrompt` is dynamic (fetches active projects per request), so it goes as a standalone entry outside any cached group. ## Caching Static Prompts Prompt caching is managed through [cache breakpoint groups](/core-concepts/cache-breakpoints) in your config, not in individual prompt classes. To cache a prompt, place it inside a group with a `cache` key in the `prompts` config: ```php // config/iris-custom.php 'prompts' => [ [ 'cache' => ['type' => 'ephemeral', 'ttl' => '1h'], 'prompts' => [ App\Prompts\IrisStaticPrompt::class, App\Extensions\Prompts\YourStaticPrompt::class, // Cached with the group ], ], App\Prompts\MemoryPrompt::class, // Not cached (dynamic content) ], ``` Individual prompt classes don't need to set `providerOptions()` for caching — any `cacheType` or `cacheTtl` values set by prompts are automatically stripped. The config is the single authority for where cache breakpoints are placed. See [Cache Breakpoints](/core-concepts/cache-breakpoints) for the full guide on configuring groups, TTL options, and the 4-breakpoint limit. --- --- url: /tools/task-delegation.md --- # Task Delegation > \[!IMPORTANT] > This feature is in **beta** and represents an initial implementation. The API and behavior may change or be removed in future releases. Iris can delegate complex, multi-step tasks to an autonomous sub-agent. Instead of executing commands one at a time, the sub-agent works independently - handling errors, iterating on solutions, and returning results when the task is complete. > \[!WARNING] > Task delegation is **disabled by default**. It requires shell commands to also be enabled, as the sub-agent uses shell commands to accomplish tasks. ## When to Use Task Delegation Task delegation shines for complex operations that require multiple steps: * **Project scaffolding**: "Set up a new Laravel project with authentication" * **Code refactoring**: "Rename the User model to Account across the codebase" * **Build processes**: "Run the full test suite and fix any failures" * **File migrations**: "Reorganize the images folder by date" * **Complex installations**: "Install and configure Tailwind CSS" For quick, single commands, Iris uses the shell tool directly. Task delegation is for work that needs autonomous problem-solving. ## Setup Enable both task delegation and shell commands in your `.env`: ```bash IRIS_SUBAGENT_ENABLED=true IRIS_SHELL_ENABLED=true ``` Optionally set a default working directory: ```bash IRIS_SUBAGENT_WORKING_DIR=/home/user/projects ``` ### Running the Agent Daemon Delegated tasks are processed by the `iris:agent` daemon — a long-running command that polls for pending tasks: ```bash php artisan iris:agent ``` The daemon picks up tasks as they're created, processes them sequentially, and handles rate limits automatically by backing off and retrying. It responds to `SIGINT` and `SIGTERM` for graceful shutdown. If you're using `composer dev`, the agent daemon is already included — no extra setup needed. > \[!IMPORTANT] > Without the agent daemon running, delegated tasks will sit in a pending state indefinitely. You can also process a single task and exit, which is useful for testing: ```bash php artisan iris:agent --once ``` ## Example Conversations ### Project Setup > **You**: "Create a new Laravel project with Breeze authentication and Tailwind" > > **Iris**: "I'll delegate this to a sub-agent. This involves several steps - creating the project, installing Breeze, running migrations, and building assets." > > *\[sub-agent works autonomously for ~2 minutes]* > > **Iris**: "Done! Here's what I did: > > * Created new Laravel project > * Installed Laravel Breeze with React > * Ran npm install and npm run build > * Ran migrations > * The app is ready at ~/projects/myapp" ### Code Refactoring > **You**: "Rename all instances of 'getUserById' to 'findUser' in my project" > > **Iris**: "I'll search for all occurrences and update them." > > *\[sub-agent searches, identifies files, makes changes]* > > **Iris**: "Updated 12 files. I also updated the corresponding test files to use the new method name. Here's a summary of changes..." ### Build and Fix > **You**: "Run the PHPStan analysis and fix any level 5 errors" > > **Iris**: "I'll run the analysis and work through any issues." > > *\[sub-agent runs PHPStan, reads errors, applies fixes, re-runs]* > > **Iris**: "Fixed 8 errors across 5 files. Most were missing return types - I added them based on the actual return values. PHPStan now passes at level 5." ## How Sub-Agents Work When Iris delegates a task: 1. **Task creation**: Iris creates a task definition with your requirements 2. **Sub-agent spawn**: A new agent starts with access to shell commands and web tools 3. **Autonomous execution**: The sub-agent works through the task, handling errors and iterating 4. **Completion**: Results are returned to Iris, who summarizes what happened The sub-agent can: * Execute shell commands (with the same security restrictions) * Search the web for documentation or solutions * Fetch content from URLs * Iterate up to 60 steps before stopping ## Monitoring Tasks You can keep tabs on delegated tasks directly from the chat header — no need to wait blindly for results. ### Task Indicator The bot icon in the header is always visible. When a task is actively running, a pink pulse badge appears on the icon. Click it to open a dropdown showing your 5 most recent tasks. ### Real-Time Tool Tracking Expand a running task to watch every tool call as it happens: * **Shell commands** appear as they execute, with arguments and completion status * **Provider tools** (web search, web fetch) are tracked with the same real-time visibility * A pulsing dot indicates a tool is in progress; a checkmark means it completed > \[!TIP] > This is a great way to understand what the sub-agent is doing and catch issues early — if you see it heading in the wrong direction, you can cancel and refine your instructions. ### Completed Task History The dropdown shows your 5 most recent tasks regardless of status. Expand a completed task to fetch its full tool call history from the server, so you can review exactly what the sub-agent did. ### Cancelling Tasks Click the stop button next to any running task to cancel it. Cancelled tasks remain visible briefly before being automatically dismissed. ## Success Criteria You can give Iris specific conditions that define completion: > **You**: "Set up ESLint with the Airbnb config. Make sure it passes on the existing codebase." > > **Iris**: *\[delegates with success criteria: "ESLint configured", "Airbnb preset installed", "No linting errors on existing code"]* The sub-agent uses these criteria to know when the task is truly done versus when it needs to keep working. ### Writing Good Criteria **Verifiable conditions work best:** * "All tests pass" - verifiable with test runner * "No TypeScript errors" - verifiable with `tsc` * "Config file exists at config/myapp.php" - verifiable with `ls` * "Database migrated successfully" - verifiable from command output **Vague conditions don't help:** * "Code is clean" - subjective * "Everything works" - unverifiable * "Looks good" - meaningless to an agent ## One Task at a Time To prevent resource exhaustion, each user can only have one active delegated task. If you try to start a new task while one is running: > **You**: "Also set up the deployment pipeline" > > **Iris**: "You already have a task in progress (setting up the Laravel project). I'll handle the deployment pipeline once that completes, or you can wait for it to finish." ## Timeouts Tasks have configurable timeouts: | Setting | Default | Maximum | |---------|---------|---------| | Task timeout | 5 minutes | 10 minutes | For long-running tasks, you can request more time: > **You**: "Run the full integration test suite - it takes about 8 minutes" > > **Iris**: *\[delegates with 10 minute timeout]* If a task times out: * The sub-agent is terminated * Partial work may remain on disk * Iris will explain what happened and suggest next steps ## What the Sub-Agent Can Access The sub-agent has access to all tools configured in `config/iris.php`, including: | Tool | Type | Purpose | |------|------|---------| | All configured tools | Custom | The full set of tools from `iris.tools` (memory, calendar, shell, etc.) | | Web search | Provider | Find documentation, solutions, or current information | | Web fetch | Provider | Retrieve content from specific URLs | All tool calls — both custom and provider — are tracked in real time and persisted for review after completion. The sub-agent inherits all shell security settings - blocked commands, blocked patterns, and environment sanitization. ## Configuration Customize sub-agent behavior in `config/iris.php`: ```php 'subagent' => [ 'enabled' => env('IRIS_SUBAGENT_ENABLED', false), 'provider' => 'anthropic', 'model' => 'claude-sonnet-4-5', 'max_steps' => 60, 'default_working_directory' => env('IRIS_SUBAGENT_WORKING_DIR'), 'request_timeout' => 120, 'default_timeout' => 300, 'max_timeout' => 600, ], ``` ### Allowing More Iterations For complex tasks that need many steps: ```php // config/iris-custom.php return [ 'subagent' => [ 'max_steps' => 100, ], ]; ``` ### Longer Default Timeout If your tasks typically run long: ```php // config/iris-custom.php return [ 'subagent' => [ 'default_timeout' => 600, // 10 minutes ], ]; ``` ## Task Delegation vs Shell Commands | Situation | Use | |-----------|-----| | Single command | Shell commands | | Quick file check | Shell commands | | Multi-step process | Task delegation | | Needs iteration and error handling | Task delegation | | Build/test/deploy workflows | Task delegation | | Interactive feedback needed | Shell commands | When in doubt, start with shell commands. Iris will suggest task delegation when a task would benefit from autonomous execution. ## Troubleshooting **Task seems stuck**: Tasks have a maximum of 60 steps. Very complex tasks may hit this limit. Try breaking the work into smaller pieces. **Unexpected results**: Ask Iris to show you what the sub-agent did. The full output is available after completion. **Timeout on long tasks**: Request a longer timeout explicitly, or break the task into phases. **"Task already in progress"**: Wait for the current task to complete, or ask Iris about its status. ## Disabling Task Delegation To disable (the default), ensure your `.env` doesn't enable it: ```bash # .env IRIS_SUBAGENT_ENABLED=false ``` Or disable just the delegation tool while keeping shell commands: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\Agent\DelegateTaskTool::class, ], ]; ``` --- --- url: /integrations/telegram-notifications.md --- # Telegram Notifications Iris can push proactive messages to your phone via Telegram. When a heartbeat check-in, calendar reminder, or scheduled follow-up fires, you'll get a Telegram notification within seconds — no need to have the web UI open. > \[!TIP] > Telegram is a one-way notification channel. You read the message on your phone and tap through to the web UI for full interaction. Two-way Telegram messaging is a separate feature on the roadmap. ## How It Works When the [heartbeat system](/core-concepts/proactive-messages) generates a proactive message, two things happen in parallel: 1. The message broadcasts to the web UI via WebSocket (existing behavior) 2. The `SendTelegramNotification` listener sends the same message to your linked Telegram chat Each notification includes the full message content and a "View in Iris" button that deep-links to the thread in the web UI. The button is an inline keyboard element — it doesn't count against Telegram's 4096-character message limit. Telegram delivery is completely isolated from the web UI. If the Telegram API is down or your chat ID is wrong, the web UI message still arrives normally. ## Setup ### 1. Create a Telegram Bot Create a bot through [BotFather](https://t.me/botfather) on Telegram: 1. Open a chat with **@BotFather** 2. Send `/newbot` and follow the prompts to name your bot 3. Copy the **bot token** (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`) 4. Note the **bot username** (e.g., `MyIrisBot`) ### 2. Configure Environment Add these variables to your `.env`: ```bash TELEGRAM_ENABLED=true TELEGRAM_BOT_TOKEN=your-bot-token-from-botfather TELEGRAM_BOT_USERNAME=YourBotUsername ``` ### 3. Register the Bot Telegraph stores bot credentials in the database and needs a webhook so your bot can handle the `/start` command during account linking. Register both with: ```bash php artisan telegraph:new-bot ``` The command prompts you for your bot token and name, then offers to set up the webhook automatically. When it asks to add a chat, you can skip that — Iris handles chat creation during account linking. > \[!TIP] > If you need to re-register the webhook later (e.g., after changing your domain), run `php artisan telegraph:set-webhook`. ### 4. Connect Your Account 1. Open Iris and go to **Settings > Telegram Notifications** 2. Click **Connect Telegram** 3. A link appears — click **Open in Telegram** (or copy the link to your phone) 4. In Telegram, tap **Start** when the bot chat opens 5. The settings page updates automatically once linking succeeds 6. Toggle **Enable Notifications** on The linking flow uses a time-limited token (expires in 10 minutes) so only authenticated Iris users can connect their Telegram account. > \[!TIP] > Click **Send Test Message** after connecting to verify everything works end-to-end. ## Message Format Notifications arrive as Telegram messages with your proactive message content in Markdown, plus an inline keyboard button: ``` Hey — just a heads up, your meeting with Sarah starts in 15 minutes. You mentioned wanting to bring up the timeline for the design review. [View in Iris] ← tappable button linking to the thread ``` ### Long Messages Telegram limits messages to 4096 characters. When a proactive message exceeds this, Iris truncates at the nearest sentence boundary and appends a continuation indicator: ``` {message content truncated at sentence boundary}... _(continued in Iris)_ [View in Iris] ``` The truncation is smart — it tries sentence boundaries first (`. `, `! `, `? `), then paragraph breaks, then word boundaries as a last resort. ## Configuration Telegram settings live in `config/connectors.php`: | Setting | Environment Variable | Description | |---------|---------------------|-------------| | `telegram.enabled` | `TELEGRAM_ENABLED` | Enable Telegram integration | | `telegram.token` | `TELEGRAM_BOT_TOKEN` | Bot token from BotFather | | `telegram.bot_username` | `TELEGRAM_BOT_USERNAME` | Bot username for generating links | ### User Settings Each user controls their own notifications in **Settings > Telegram Notifications**: | Setting | Description | |---------|-------------| | **Connect / Disconnect** | Links or unlinks a Telegram chat | | **Enable Notifications** | Toggle delivery on or off (only available when connected) | | **Send Test Message** | Sends a test notification to verify the connection | Disconnecting clears the chat link and disables notifications. Toggling notifications off preserves the link so you can re-enable without reconnecting. ## Relationship to Boundaries Telegram notifications inherit the [heartbeat boundary system](/core-concepts/proactive-messages#setting-up-proactive-messages) for free. When a boundary is active (Quiet Hours, Do Not Disturb, Temporary Silence), the heartbeat system doesn't generate proactive messages at all — so there's nothing to notify about. If you want proactive messages in the web UI but not on your phone, toggle **Enable Notifications** off in Telegram settings. The boundary system and notification toggle serve different purposes: * **Boundaries** control whether Iris reaches out at all * **Notification toggle** controls whether the message also goes to Telegram ## Disabling Telegram Notifications You can disable at two levels: * **Per-user**: Toggle off **Enable Notifications** in Settings > Telegram Notifications * **System-wide**: Set `TELEGRAM_ENABLED=false` in your `.env` — the settings page will show "not available" and no notifications fire for any user --- --- url: /integrations/text-to-speech.md --- # Text-to-Speech Listen to assistant messages with natural-sounding audio using ElevenLabs. Click the play button on any assistant message to hear it read aloud. ## Features * **One-Click Playback**: Play button appears on every assistant message * **Natural Voices**: High-quality audio from ElevenLabs * **Stop Anytime**: Click again to stop playback mid-sentence * **Rate Limited**: 10 requests per minute to manage API costs ## Setup Text-to-speech requires an ElevenLabs API key. Add these to your `.env`: ```bash IRIS_TTS_ENABLED=true ELEVENLABS_API_KEY=your-elevenlabs-key ``` That's it - the play button will appear on assistant messages automatically. ### Getting an ElevenLabs API Key 1. Create an account at [elevenlabs.io](https://elevenlabs.io) 2. Navigate to your profile settings 3. Copy your API key from the API section ElevenLabs offers a free tier with limited characters per month, which is enough for casual use. ## Usage After enabling TTS, you'll see a speaker icon next to the copy button on assistant messages: 1. Click the **speaker icon** to generate and play audio 2. The icon shows a **loading spinner** while generating 3. The icon changes to a **stop button** during playback 4. Click **stop** to end playback early ### What Gets Converted The text content of the assistant's message is sent to ElevenLabs. Code blocks, tool calls, and other non-text elements are excluded from the audio. > \[!TIP] > For long messages, audio generation may take a few seconds. The loading spinner indicates the API is processing your request. ## Configuration TTS settings are defined in `config/iris.php`: | Setting | Value | Description | |---------|-------|-------------| | `tts.enabled` | env-controlled | Set via `IRIS_TTS_ENABLED` | | `tts.model` | `eleven_multilingual_v2` | Supports multiple languages | | `tts.voice` | (configured) | Voice ID for speech generation | ### Changing the Voice To use a different ElevenLabs voice, override the voice ID in `config/iris-custom.php`: ```php // config/iris-custom.php return [ 'tts' => [ 'voice' => 'your-preferred-voice-id', ], ]; ``` Find voice IDs in your [ElevenLabs Voice Library](https://elevenlabs.io/app/voice-library). > \[!TIP] > See [Customization](/advanced/customization) for details on how `iris-custom.php` merges with core config. ## Rate Limiting TTS requests are rate limited to 10 per minute per user. This prevents accidental cost overruns and API abuse. If you hit the limit, wait a moment before trying again. ## How It Works 1. **Request**: You click the play button on an assistant message 2. **Generate**: The backend sends the message text to ElevenLabs 3. **Stream**: ElevenLabs returns audio data 4. **Play**: The browser plays the audio using the Web Audio API Audio is generated on-demand - there's no caching, so each click makes an API call. ## Costs ElevenLabs pricing is based on characters generated: | Plan | Characters/month | Notes | |------|-----------------|-------| | Free | 10,000 | Good for testing | | Starter | 30,000 | ~$5/month | | Creator | 100,000 | ~$22/month | A typical assistant message is 200-500 characters. The free tier supports roughly 20-50 message playbacks per month. > \[!WARNING] > Long conversations with frequent TTS use can consume characters quickly. Monitor your ElevenLabs dashboard for usage. ## Troubleshooting **Play button doesn't appear**: Verify `IRIS_TTS_ENABLED=true` in your `.env` and rebuild the frontend with `npm run build`. **"Text-to-speech is not enabled" error**: The backend config isn't detecting the environment variable. Check your `.env` and clear the config cache: `php artisan config:clear`. **Audio doesn't play**: Check your browser's console for errors. Some browsers block autoplay - try clicking the play button again. **"Too many requests" error**: You've hit the rate limit. Wait a minute before trying again. **ElevenLabs API errors**: Verify your API key is correct and your account has available characters. ## Disabling Text-to-Speech To disable TTS entirely, set the environment variable: ```bash IRIS_TTS_ENABLED=false ``` The play button will no longer appear on assistant messages. --- --- url: /checkout-success.md --- --- --- url: /core-concepts/thread-briefs.md --- # Thread Briefs Thread Briefs give Iris peripheral awareness of what's happening across all your conversations. Each brief is a short, 3-6 sentence digest of a thread's current state — what you're working on, key decisions made, and how you're feeling about it. They're generated automatically in the background and injected into other threads' system prompts, so Iris can connect ideas, reference related work, and avoid asking questions you've already answered elsewhere. ## Why Thread Briefs? Without briefs, every thread is an island. [Memories](/core-concepts/memory-system) and [Truths](/core-concepts/truths) capture *who you are* — your preferences, relationships, and stable facts — but they don't capture *what you're actively working on*. If you're debugging a database migration in one thread and designing an API in another, Iris has no way to connect the two. Thread Briefs fill that gap. They're lightweight enough to include in every request (~100-200 tokens per brief) but rich enough to give Iris meaningful cross-thread context. **What briefs enable:** * **Cross-thread connections**: Iris can suggest that the schema decisions in Thread A affect the API design in Thread B * **No redundant questions**: If you've established your database choice in one thread, Iris won't re-ask it in another * **Smarter proactive messages**: The [heartbeat](/core-concepts/proactive-messages) uses briefs to understand what you're working on across threads, replacing noisy message fragments with semantic understanding ## How They Work Briefs generate automatically as you chat. After every 2 assistant responses (configurable), a background job dispatches that reads the thread's recent messages, previous brief, and latest [summary](/core-concepts/summarization) (if one exists), then produces an updated digest using a fast, inexpensive model. ### Generation Flow 1. You exchange messages in a thread 2. After the configured number of assistant responses, `GenerateThreadBrief` dispatches 3. The job loads the last ~10 messages, the existing brief (for continuity), and the latest conversation summary (for structured context like decisions and goals) 4. A fast model (Haiku by default) produces a 3-6 sentence digest 5. The brief is stored on the thread and immediately available to other threads ### What a Brief Captures Each brief covers four dimensions: * **Topic**: What the thread is about right now * **Key decisions and facts**: Important choices or information established in the conversation * **Current status**: Where things stand — in progress, stuck, resolved * **Emotional context**: How you're feeling — frustrated, energized, in flow, stuck The emotional dimension is particularly valuable for [proactive messaging](/core-concepts/proactive-messages). It helps Iris decide whether to reach out ("you've been stuck for a while — want a hand?") or give you space ("you're in flow — no interruption needed"). ### Brief Continuity Each generation includes the previous brief as input, so briefs evolve naturally as conversations progress. Early in a thread, a brief might be sparse: "Exploring options for background job monitoring in Laravel. Early conversation, no decisions yet." As the thread develops, it becomes richer and more specific. When a conversation summary exists, the brief generator also incorporates its structured data — key decisions, accomplishments, active goals, and emotional markers. This means briefs stay informed even when the last 10 messages are a narrow window into a longer conversation. ## Cross-Thread Context The primary use of briefs is cross-thread awareness during regular chat. A `CrossThreadContextPrompt` loads briefs from your most recently active threads (up to 5 by default) and injects them into the [system prompt](/core-concepts/system-prompts). Here's what Iris sees: ``` ## Other Active Conversations - **Database Migration Strategy** (last active: Apr 27, 1:00pm EDT): TJ is migrating from MySQL to PostgreSQL. Decided on staged rollout using Laravel migrations. Currently working on the user table schema. TJ is frustrated with some foreign key constraint issues but making progress. - **API Rate Limiting** (last active: Apr 26, 3:15pm EDT): Discussed rate limiting approaches for the public API. Settled on token bucket with Redis. TJ wants 100 req/min for free tier. TJ was energized and in flow state during this design session. ``` **Key design decisions:** * **Current thread excluded**: Iris never sees a brief of the conversation you're actively in — that would be redundant and waste tokens * **Static timestamps**: Timestamps use a fixed format (e.g., "Apr 27, 1:00pm EDT") rather than relative times ("2 hours ago"). This is critical for [prompt caching](/core-concepts/cache-breakpoints) — relative timestamps change every minute and invalidate the cache, while static timestamps only change when a thread actually updates * **Empty is silent**: When no other threads have briefs, the prompt renders nothing — no "No active threads" placeholder * **User-scoped**: The query only returns threads belonging to the authenticated user ## Heartbeat Integration The [heartbeat](/core-concepts/proactive-messages) uses the same briefs to understand what you're working on when deciding whether to proactively message you. Before briefs, the heartbeat loaded raw message fragments — the last 10 turns from your 3 most active threads, each truncated to 200 characters. This was noisy, token-expensive, and semantically shallow. With briefs, the heartbeat gets dense, meaningful context: what each thread is about, what decisions were made, and how you're feeling. This produces better decisions about when to reach out and what tone to use. Threads without briefs are excluded from heartbeat context — there's no fallback to raw messages. ## Graceful Degradation Briefs are designed to degrade silently: * **New threads** don't have briefs until enough messages are exchanged — other threads and the heartbeat work with what's available * **Failed generation** leaves the previous brief intact (or null if none existed) — no error surfaces to you * **Disabled briefs** (`iris.briefs.enabled = false`) turns everything off — no generation, no cross-thread injection, no heartbeat brief context ## Configuration All brief settings live under `iris.briefs` in `config/iris.php`: | Setting | Default | Description | |---------|---------|-------------| | `briefs.enabled` | `true` | Master toggle for all brief-related behavior | | `briefs.frequency` | `2` | Assistant responses between brief generations | | `briefs.threshold` | `2` | Minimum assistant responses before first brief | | `briefs.max_threads` | `5` | Maximum threads shown in cross-thread context | | `briefs.provider` | `anthropic` | Prism provider for brief generation | | `briefs.model` | `claude-haiku-4-5` | Model for generating briefs | | `briefs.timeout` | `15` | Timeout in seconds for generation | ### Customizing ```php // config/iris-custom.php return [ 'briefs' => [ 'frequency' => 3, // Generate less often 'max_threads' => 3, // Show fewer threads in cross-thread context ], ]; ``` ### Disabling Briefs Set the environment variable or config: ```bash # .env IRIS_BRIEFS_ENABLED=false ``` When disabled, no `GenerateThreadBrief` jobs dispatch, the cross-thread context prompt renders empty, and the heartbeat includes no thread brief content. > \[!TIP] > Brief generation uses a fast, inexpensive model by default. At roughly $0.001 per brief, the cost is negligible — even with frequent generation across many threads. ## How Briefs Relate to Other Systems | System | Relationship | |--------|-------------| | [Memories](/core-concepts/memory-system) | Memories capture stable facts about you. Briefs capture what you're actively working on. They're complementary — memories persist, briefs are ephemeral snapshots. | | [Summaries](/core-concepts/summarization) | Summaries are rich, structured compactions of conversation history within a thread. Brief generation uses the latest summary as input when available, distilling it into a few sentences for cross-thread use. | | [Proactive Messages](/core-concepts/proactive-messages) | The heartbeat uses briefs instead of raw message fragments for "read the room" context. Emotional markers in briefs directly inform outreach decisions. | | [System Prompts](/core-concepts/system-prompts) | `CrossThreadContextPrompt` is registered in the pinned content cache group, sharing an ephemeral cache breakpoint. | ## See Also * [Threads](/core-concepts/threads) — how threads organize conversations * [System Prompts](/core-concepts/system-prompts) — how briefs are injected into the prompt pipeline * [Proactive Messages](/core-concepts/proactive-messages) — how the heartbeat uses briefs * [Background Jobs](/architecture/background-jobs) — how `GenerateThreadBrief` runs --- --- url: /core-concepts/threads.md --- # Threads Threads organize your conversations into separate contexts. Each thread maintains its own conversation history, summaries, and settings — so a thread about work stays focused on work, while a thread about cooking stays focused on recipes. ## How Threads Work Every conversation in Iris belongs to a thread. When you send a message, it's recorded in the active thread. Summaries, context recall, and streaming all operate within that thread's boundary — nothing leaks between threads. This isolation means you can have multiple ongoing conversations with Iris without context from one bleeding into another. ## The Default Thread When you first use Iris, a **Default** thread is automatically created and pinned to the top of your sidebar. This ensures you always have a thread ready to go. If you delete your last remaining thread, Iris creates a new Default thread immediately — you'll never end up with an empty sidebar. ## Creating Threads Click the **New Thread** button in the sidebar to create a fresh thread. New threads start unnamed and appear in the sidebar immediately. Once you've exchanged a few messages, Iris automatically generates a name based on the conversation content. ## Thread Naming Iris auto-names threads after a few messages (default: 4) so you don't have to. The naming uses a fast, lightweight model to generate a short, descriptive title based on the conversation so far. **How auto-naming works:** 1. You send messages in a new thread 2. Once the message count reaches the naming threshold, a background job queues 3. The job reads recent messages and generates a concise name 4. The name appears in the sidebar across all your connected devices **Manual renaming:** You can rename any thread at any time through the sidebar context menu. Once you rename a thread manually, auto-naming won't overwrite your choice. ## Pinning Threads Pin important threads to keep them at the top of your sidebar. Pinned threads appear in a dedicated "Pinned" section above your regular threads. To pin or unpin a thread, use the context menu (hover over the thread name in the sidebar). ## Unread Indicators When a new message arrives in a thread you're not currently viewing — like a proactive message or a completed delegated task — a small indicator appears next to the thread name. Clicking the thread marks it as read. ## Deleting Threads Delete a thread through the sidebar context menu. A confirmation dialog shows the thread name so you don't accidentally remove the wrong one. > \[!WARNING] > Deleting a thread permanently removes all its conversations and summaries. This action cannot be undone. After deletion, Iris navigates you to your most recently active thread. If no threads remain, a new Default thread is created automatically. ## Thread-Scoped Features Threads aren't just visual organizers — they provide real isolation across Iris's core systems: ### Conversation History Each thread maintains its own message history. History is loaded token-budget-first — Iris pulls the most recent turns until the available token budget is exhausted, so each thread gets its full context allocation independent of the others. ### Summarization [Summaries](/core-concepts/summarization) are generated and chained per thread. A long thread about a work project builds its own summary chain, separate from your other threads. The summarization threshold and buffer settings apply independently to each thread. ### Streaming and Broadcasts Response streams are scoped to the active thread. When Iris responds in one thread, only clients viewing that thread receive the stream events. This prevents messages from appearing in the wrong thread if you have multiple tabs open. ### Pinned Skills Each thread can have its own set of [pinned skills](/tools/agent-skills#pinning-skills-to-threads) — specialized knowledge injected into the system prompt for every message in that thread. Configure pinned skills through the thread settings modal (gear icon in the chat header). ### Thread Briefs Each thread automatically generates a [brief](/core-concepts/thread-briefs) — a 3-6 sentence digest of what the thread is about, key decisions made, and emotional context. Briefs are injected into other threads' system prompts, giving Iris cross-thread awareness without loading raw messages. This means if you're discussing a database migration in one thread and API design in another, Iris can connect the two — suggesting that schema decisions in one affect endpoints in the other. See [Thread Briefs](/core-concepts/thread-briefs) for the full details. ### Memories and Truths [Memories](/core-concepts/memory-system) and [truths](/core-concepts/truths) are **stored globally** — your name, preferences, and relationships are available regardless of which thread you're in. Iris always knows who you are, even in a brand-new thread. However, memory *recall* is thread-scoped. When Iris retrieves relevant memories, the search queries are generated from the current thread's conversation context — not from all threads combined. This means memories about cooking surface when you're discussing recipes in one thread, without being influenced by a debugging conversation in another thread. The full memory pool is always searchable; only the question "what's relevant right now?" is scoped to the thread you're in. ## Thread Settings Open the thread settings modal by clicking the gear icon in the chat header. Currently, thread settings let you manage [pinned skills](/tools/agent-skills#pinning-skills-to-threads) — skills whose full content is injected into the system prompt for every message in that thread. ## Configuration Thread behavior is configured in `config/iris.php`: | Setting | Default | Description | |---------|---------|-------------| | `threads.naming_threshold` | 4 | Messages before auto-naming triggers | | `threads.naming_provider` | `anthropic` | Prism provider for name generation | | `threads.naming_model` | `claude-haiku-4-5` | Model for generating thread names | | `threads.naming_timeout` | 15 | Timeout in seconds for the naming job | ### Customizing Thread Naming ```php // config/iris-custom.php return [ 'threads' => [ 'naming_threshold' => 6, // Wait for more messages before naming ], ]; ``` > \[!TIP] > Auto-naming uses a fast, inexpensive model by default. If you want more creative names, you could swap in a larger model — but the cost-per-name goes up accordingly. --- --- url: /tools/overview.md --- # Tools Overview Tools are how Iris interacts with the world beyond conversation. They enable actions like storing memories, managing calendars, generating images, and searching the web. ## What Are Tools? Tools are functions that Iris can invoke during a conversation. When Iris determines an action is needed, it calls the appropriate tool, receives a result, and incorporates it into the response. ``` User: "Remember that I prefer dark mode" ↓ Iris calls: store_memory(content="User prefers dark mode", type="preference") ↓ Tool returns: "Memory stored successfully" ↓ Iris: "Got it! I'll remember you prefer dark mode." ``` This happens seamlessly -users just ask for things, and Iris decides when to use tools. ## Tool Types Iris uses three types of tools: ### Custom Tools Tools defined in `app/Tools/`, built using Prism's `Tool` class. These are application-specific tools that run server-side: ```php // config/iris.php 'tools' => [ // Truth tools - stable, core facts StoreTruthTool::class, SearchTruthTool::class, UpdateTruthTool::class, DeleteTruthTool::class, // Memory tools - contextual information StoreMemoryTool::class, SearchMemoryTool::class, UpdateMemoryTool::class, DeleteMemoryTool::class, // Calendar tools ListCalendarEventsTool::class, CreateCalendarEventTool::class, UpdateCalendarEventTool::class, DeleteCalendarEventTool::class, // Image generation GenerateImageTool::class, ], ``` Custom tools have full access to Laravel's services -they can query databases, call APIs, write files, or perform any server-side operation. Add your own tools via `config/iris-custom.php` -they'll be appended to the list. See [Custom Tools](/tools/custom-tools) for details. ### Provider Tools Built-in tools provided by Anthropic that run on their infrastructure: ```php // config/iris.php 'provider_tools' => [ ['type' => 'web_fetch_20250910', 'name' => 'web_fetch'], ['type' => 'web_search_20250305', 'name' => 'web_search'], ], ``` | Tool | Purpose | |------|---------| | `web_search` | Search the web for current information | | `web_fetch` | Fetch content from a specific URL | Provider tools are stored as arrays (rather than classes) for config caching compatibility. ### System Tools (Beta) Tools that interact with the host operating system or spawn autonomous agents. These are initial implementations and the API may change or be removed in future releases. ```php // config/iris.php 'tools' => [ // ... other tools ... RunShellCommandTool::class, DelegateTaskTool::class, ], ``` | Tool | Purpose | |------|---------| | `run_shell_command` | Execute shell commands on the host system | | `delegate_task` | Delegate complex tasks to a sub-agent | | `read_file` | Read a file within the agent's workspace | | `write_file` | Write or create a file within the agent's workspace | | `edit_file` | Replace a string within a previously-read workspace file | | `grep` | Search file contents by regex pattern within the workspace | | `glob` | List files matching a glob pattern within the workspace | > \[!NOTE] > `run_shell_command` has full host access — it can read and write anywhere the process user can reach. The five filesystem tools (`read_file`, `write_file`, `edit_file`, `grep`, `glob`) are workspace-scoped: every path is resolved and validated against the agent's workspace root, so they can never access files outside it. All system tools are disabled by default and require explicit enablement via environment variables. See [Shell Commands](/tools/shell-commands), [Task Delegation](/tools/task-delegation), and [Filesystem Tools](/tools/filesystem-tools) for details. ### Agent Skills [Agent Skills](https://agentskills.io) extend Iris with specialized knowledge and workflows. Skills are loaded from `.agents/skills/` and can be activated on-demand or [pinned to threads](/tools/agent-skills#pinning-skills-to-threads) for always-on expertise: ```php // config/iris.php 'tools' => [ // ... other tools ... ActivateSkillTool::class, ], ``` Pinned skills inject their full content into the [system prompt](/core-concepts/system-prompts) for every message in the thread, so you don't need to wait for Iris to activate them. See [Agent Skills](/tools/agent-skills) for details. ### When to Use Which | Use case | Tool type | |----------|-----------| | Access your data (memories, calendar) | Custom tool | | Call your APIs or services | Custom tool | | Store or modify application state | Custom tool | | Search the web for current information | Provider tool | | Fetch public web pages | Provider tool | | Execute CLI commands or scripts | System tool | | Complex multi-step file operations | System tool (delegate\_task) | ## The Agentic Loop Iris operates as an **agent**, meaning it can use multiple tools in sequence before providing a final response. This happens automatically -Iris decides what tools to use based on the request. ### How It Works ``` ┌─────────────────────────────────────────────────────────────┐ │ User Message │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Iris evaluates: "Does this require tools?" │ │ │ │ If yes → call tool(s) │ │ If no → generate response │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────┴───────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────────┐ │ Tool Call │ │ Generate │ │ │ │ Response │ └─────────────┘ └─────────────────┘ │ ▼ ┌─────────────┐ │ Tool Result │ └─────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Evaluate again: "More tools needed?" │ │ │ │ If yes → call more tools (loop) │ │ If no → generate final response │ └─────────────────────────────────────────┘ ``` ### Example: Multi-Step Task > **You**: "Schedule a meeting with John tomorrow at 2pm and remind me to prepare the quarterly report" Iris might: 1. Call `create_calendar_event(title: "Meeting with John", ...)` → Event created 2. Call `store_memory(content: "Prepare quarterly report before meeting with John", ...)` → Memory stored 3. Generate response: "Done! I've scheduled your meeting with John for 2pm tomorrow and I'll remember you need to prepare the quarterly report." ### Step Limits The `agent.max_steps` configuration (default: 60) limits iterations to prevent runaway loops. Each tool call counts as one step. Most requests use 1-5 steps. ## Building Tools Iris tools are built using Prism's `Tool` class. For complete documentation on tool anatomy, parameter types, and return values, see the [Prism Tools Documentation](https://prismphp.com/docs/core-concepts/tools-function-calling). For Iris-specific patterns and examples, see [Custom Tools](/tools/custom-tools). ## Stream Events Tool activity appears in the response stream in real-time, allowing the frontend to show what Iris is doing: | Event | When | Example | |-------|------|---------| | `ToolCallEvent` | Tool invocation started | "Storing memory..." | | `ToolResultEvent` | Tool execution completed | "Memory stored" | | `ProviderToolEvent` | Provider tool activity | "Searching the web..." | The frontend uses these events to show tool activity chips and status indicators during streaming. > \[!NOTE] > Events stream via WebSockets for real-time delivery. If a connection drops briefly, events are stored and replayed when the client reconnects. ## Disabling Tools To disable specific tools without modifying core config: ```php // config/iris-custom.php return [ 'disabled_tools' => [ App\Tools\GenerateImageTool::class, // Disable image generation ], ]; ``` To disable all provider tools: ```php // config/iris-custom.php return [ 'provider_tools' => [], ]; ``` --- --- url: /core-concepts/truth-consolidation.md --- # Truth Consolidation Over time, Truths can accumulate semantic overlap. Multiple Truths saying essentially the same thing - like "has a son named Ellis," "father of Ellis," and "child named Ellis" - dilute your context window without adding value. Truth consolidation merges these redundant Truths into denser, more useful representations. ## Why Consolidation Matters * **Reduces redundancy** - Similar Truths merged into one * **Preserves context budget** - Fewer Truths means more room for memories * **Improves coherence** - Related facts combined into comprehensive statements Unlike [Memory Consolidation](/core-concepts/memory-consolidation), which runs daily on new memories, Truth consolidation is more conservative. Truths are already distilled facts - they've earned their place through behavioral evidence. We consolidate only when there's clear semantic overlap. ## Protection Model Not all Truths are consolidatable. The system protects Truths based on their source and status: | Truth Type | Consolidatable? | Reason | |------------|-----------------|--------| | **Promoted** (unpinned, under max generation) | Yes | System-created, can be refined | | **User Created** | No | Explicit user intent must be preserved | | **Agent Created** | No | Created during conversation for specific reasons | | **Pinned** | No | User marked as important | | **At Max Generation** | No | Already been refined multiple times | This protection model matches the refinement system - if a Truth can't be automatically refined, it can't be automatically consolidated. ## How It Works Consolidation uses a two-phase job architecture for efficient parallel processing: **Phase 1: Cluster Building** 1. Find Truths with high similarity (≥0.75) 2. Group them into clusters using greedy clustering 3. Dispatch one job per cluster **Phase 2: Cluster Processing** 1. Each cluster job asks the LLM if Truths should merge 2. If approved, create consolidated Truth and soft-delete originals 3. If rejected, Truths remain separate The LLM may keep Truths separate if they contain genuinely distinct information, reference different people, or if merging would lose important nuance. ## Example **Input Truths:** * "Has a son named Ellis" (Gen 0, Score: 45) * "Father of a child named Ellis" (Gen 1, Score: 32) * "Ellis is the user's son" (Gen 0, Score: 28) **Result:** * "Has a son named Ellis" (Gen 2, Score: 45) The consolidated Truth: * Takes the clearest, most informative wording * Inherits the highest distillation score from sources * Tracks its lineage for the history view ## Generation Tracking Consolidation tracks how many times a Truth has been merged through **generation tracking**. This enables organic evolution while preventing runaway over-consolidation. ### How Generations Work ``` Gen 0 ─┬─► Gen 1 ─┬─► Gen 2 ─┬─► Gen 3 ─┬─► Gen 4 ─┬─► Gen 5 (max) │ │ │ │ │ Original First Re-consolidation continues... Truths merge ``` | Generation | Description | |------------|-------------| | **Gen 0** | Original Truths from promotion or manual creation | | **Gen 1** | First consolidation (merging original Truths) | | **Gen 2-4** | Re-consolidation of already-consolidated Truths | | **Gen 5** | Maximum - these Truths won't be re-consolidated | When Truths are consolidated, the new Truth's generation is calculated as `max(source generations) + 1`. ### Re-Consolidation Unlike systems that only consolidate original items, Iris can re-consolidate already-merged Truths. This allows Truths to evolve naturally over time as more related information is gathered. > \[!IMPORTANT] > The LLM applies extra scrutiny when re-consolidating. High-generation Truths already pack dense information, so only truly redundant Truths are merged. ### Generation Limits Generation 5 is the hard limit. Truths reaching this generation are excluded from future consolidation to prevent: * Over-abstraction losing important details * Runaway consolidation chains * Truth content becoming too generic ## Running Consolidation By default, consolidation dispatches jobs to the queue for parallel processing: ```bash # Queue batch for all users (default behavior) php artisan iris:consolidate-truths # Run synchronously (useful for debugging) php artisan iris:consolidate-truths --sync # Preview without changes (forces sync) php artisan iris:consolidate-truths --dry-run # Single user php artisan iris:consolidate-truths --user=1 # Custom similarity threshold php artisan iris:consolidate-truths --threshold=0.70 ``` ### Dry Run Mode The `--dry-run` flag shows what would be consolidated without making changes: ```bash php artisan iris:consolidate-truths --user=1 --dry-run ``` Output shows each cluster with the decision (MERGE or KEEP SEPARATE), the LLM's reasoning, and proposed content if merging. ### Processing Batches When running in queue mode, the command returns a batch ID: ```bash php artisan iris:consolidate-truths # Dispatched batch: 9c3b5f2a-... # Process jobs (Horizon should already be running) php artisan horizon # Retry failed jobs in a batch php artisan queue:retry-batch 9c3b5f2a-... ``` Use Laravel Horizon for monitoring batch progress in a UI. ### Scheduled Runs Iris automatically schedules consolidation in `bootstrap/app.php`: ```php // Weekly truth consolidation (after memory consolidation) $schedule->command('iris:consolidate-truths') ->weeklyOn(Schedule::SUNDAY, '05:00') ->withoutOverlapping() ->runInBackground(); ``` Truths accumulate slower than memories since they only come through distillation. Weekly consolidation is sufficient to keep redundancy in check. ## Job Architecture Consolidation uses two job types for efficient processing: ### ConsolidateUserTruths Handles Phase 1 for a single user: * Filters to consolidatable Truths (promoted, unpinned, under max gen) * Builds clusters based on semantic similarity * Dispatches cluster jobs for Phase 2 * 60 second timeout ### ConsolidateTruthCluster Handles Phase 2 for a single cluster: * Reconstructs cluster from Truth IDs * Calls LLM to review and decide on merging * Executes consolidation if approved * 120 second timeout * Rate-limited to prevent API overload This architecture prevents timeout issues with large Truth sets by processing clusters independently. ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `truth_consolidation.similarity_threshold` | 0.75 | Minimum similarity to cluster | | `truth_consolidation.max_cluster_size` | 5 | Max Truths per cluster | | `truth_consolidation.min_cluster_size` | 2 | Min Truths to form a cluster | | `truth_consolidation.timeout` | 120 | Job timeout in seconds | | `truth_consolidation.model` | claude-sonnet-4-5 | Model for merge decisions | | `truth_consolidation.jobs_per_minute` | 10 | Rate limit for queued jobs | The 0.75 threshold is lower than memory consolidation (0.80) because Truths are already distilled - semantic overlap is more likely to be true redundancy rather than related-but-distinct information. > \[!TIP] > Use `--dry-run` with different `--threshold` values to find the right balance for your data before running actual consolidation. ## Rate Limiting Consolidation jobs are rate-limited to prevent overwhelming LLM APIs: 1. **Preventive**: Jobs throttled to `jobs_per_minute` limit 2. **Reactive**: If rate limited by the API, jobs automatically retry after the limit resets using Prism's `resetsAt` timing 3. **Time-based retries**: Jobs use `retryUntil` with a 2-hour window, allowing unlimited rate-limit releases without failing Adjust `truth_consolidation.jobs_per_minute` in `config/iris-custom.php` based on your API tier. > \[!NOTE] > Only the cluster jobs are rate-limited since they make LLM calls. The parent user jobs run without throttling. --- --- url: /core-concepts/truths.md --- # Truths Truths are the stable, core facts Iris knows about you. Unlike regular memories that capture contextual details, Truths represent distilled knowledge that's relevant across conversations - your name, key relationships, fundamental preferences, and important life facts. ## Why Truths? Regular memories excel at capturing contextual information, but some facts about you are universally relevant. You shouldn't need to remind Iris of your name or that you have a partner every time you start a new conversation. The challenge is identifying which memories deserve this "always available" status. Rather than guessing at extraction time, Truths are **earned through behavioral evidence** - memories that consistently prove useful across many conversations naturally get promoted. ## How Truths Work Truths sit in a separate layer above regular memories: ``` Conversations → Memories → Truths ↓ (semantic search) ``` When Iris retrieves context for a conversation: 1. **Truths** are evaluated first - pinned Truths are always included, others are ranked by relevance to the current conversation 2. **Memories** are then searched semantically based on what you're discussing This means even Truths get contextually ranked. If you have 10 Truths but only room for 5, the most relevant ones to your current topic surface. Your name is always relevant; your coffee preference might not be when debugging code. ## Truth Sources Truths come from three places: | Source | Description | |--------|-------------| | **Promoted** | Automatically distilled from memories that prove consistently useful | | **User Created** | Manually added through the Truths UI | | **Agent Created** | Iris creates these during conversation when it identifies core facts | ### Automatic Promotion (Distillation) The distillation process runs nightly and evaluates memories based on behavioral evidence: * **Access frequency** - How often was this memory retrieved? * **Access recency** - Is it still being accessed regularly? * **Consolidation generation** - Has it been refined through memory consolidation? Memories must meet **both** thresholds to become candidates: 1. **Percentile threshold** - In the top N% of memories by access count 2. **Absolute threshold** - Minimum number of accesses (default: 10) This dual-threshold approach prevents premature promotion. A memory accessed twice might be in the top 20% if you only have 10 memories, but it hasn't really proven its value yet. An LLM then analyzes each candidate to determine if it contains a stable, generalizable fact versus an episodic event. For example, "User prefers TypeScript over JavaScript" is promotable - it's a stable preference. But "User was debugging a TypeScript error yesterday" is not - it's a temporal event that will become irrelevant. ## Truth Refinement When new evidence supports an existing Truth, it gets **refined** - updated to incorporate the new information while maintaining its core meaning. This lets Truths evolve naturally as Iris learns more about you. For example, if a Truth says "Works as a software developer" and new conversations reveal you're specifically a "senior backend engineer at a fintech company", refinement would update the Truth to be more precise. ### Generation Tracking Each Truth tracks its **generation** - how many times it's been refined: | Generation | Meaning | |------------|---------| | 0 | Original Truth (directly from promotion or manual creation) | | 1 | Refined once with new evidence | | 2+ | Multiple refinements | | Max (5) | Cannot be refined further | Generation tracking helps you understand how "derived" a Truth is from its original evidence. A generation 0 Truth is closely tied to concrete observations. A higher generation Truth has been through multiple refinement cycles. > \[!NOTE] > Truths at the maximum generation (default: 5) are protected from further refinement. This prevents Truths from drifting too far from their original evidence. ### Temporal Fact Handling Some Truths contain information that changes over time. The refinement system understands temporal updates: * **Age-related**: "Has a 12-year-old child" updates to "Has a 13-year-old child" when new evidence shows the child's birthday passed * **Job/Role**: "Works as senior engineer" updates to "Works as staff engineer" on promotion * **Location**: "Lives in Brooklyn" updates to "Lives in Queens" after a move * **Status**: "Dating partner" updates to "Married to partner" after a wedding These aren't treated as contradictions - they're natural progressions where newer information supersedes older information. ## Conflict Detection Sometimes new evidence genuinely contradicts an existing Truth. Rather than blindly merging conflicting information, Iris detects contradictions and handles them appropriately. ### How Conflicts Work When the refinement process encounters new evidence that contradicts an existing Truth: 1. **Analysis** - An LLM evaluates whether the new evidence contradicts or enriches the Truth 2. **Evidence Strength** - The new evidence is scored based on access count, recency, and consolidation 3. **Resolution** - Based on the evidence strength and Truth protection status, the conflict is either auto-resolved or flagged for review ### Evidence Strength Calculation New evidence is scored on a 0-27+ scale: | Factor | Points | |--------|--------| | Access count | 1 point per access (capped at 20) | | Recency bonus | +5 if accessed within 7 days | | Consolidation bonus | +2 per consolidation generation | ### Resolution Rules | Condition | Resolution | |-----------|------------| | **Protected Truth** (user-created, agent-created, or pinned) | Always flagged for review | | **Strong evidence** (15+ points) against unprotected Truth | Auto-updated | | **Weak evidence** against any Truth | Flagged for review | ### Managing Conflicts The Conflicts page in the UI shows all flagged conflicts. For each conflict, you'll see: * The existing Truth content * The new evidence that triggered the conflict * A proposed update (what the system thinks the Truth should become) * The reasoning behind the proposed change * The evidence strength score You can resolve conflicts by: * **Accept** - Use the proposed content as-is * **Reject** - Keep the original Truth, discard the new evidence * **Merge** - Edit the proposed content before accepting > \[!TIP] > The merge option lets you tweak the AI's proposed update. Sometimes it's close but not quite right - editing saves time compared to rejecting and manually updating. ## Truths About Other People Truths can capture stable facts about people in your inner circle - your partner, children, or close family. These are valuable context that Iris should remember. ### Subject Preservation When a Truth is about someone other than you, the subject must be preserved in the Truth statement: | Memory | Correct Truth | Wrong Truth | |--------|---------------|-------------| | "Partner had a doctor appointment about a chronic condition" | "Partner is managing a chronic health condition" | "Is managing a chronic health condition" | | "Son was diagnosed with ADHD" | "User's son has ADHD" | "Has ADHD" | The wrong examples are ambiguous - they could be interpreted as being about you rather than the person they actually describe. ### Category Hints The memory's category provides context for subject identification: * **Relationships** - High chance the memory is about someone else * **Health** - Could be about you or someone close to you * **Personal/Professional/Preferences/Goals/Hobbies** - Usually about you ## Pinning You can pin any Truth to ensure it's always included in context, regardless of relevance scoring. This is useful for: * Facts that define how Iris should address or interact with you * Information important enough that it should never be filtered out * Core identity facts you want guaranteed in every conversation Pinned Truths are unlimited - if you pin 20 Truths, all 20 will be included. Use this thoughtfully since it affects your context window budget. Pinning also protects promoted Truths from automatic refinement. User-created and agent-created Truths are already protected by default. > \[!NOTE] > Pinned Truths are always included in context regardless of relevance scoring. User-created and agent-created Truths are already protected from automatic refinement, so pinning them is only necessary if you want guaranteed inclusion. ## Configuration | Setting | Default | Description | |---------|---------|-------------| | `truths.max_dynamic` | 7 | Maximum non-pinned Truths to include per conversation | | `truths.percentile_threshold` | 5 | Top N% of memories by access count are candidates for promotion | | `truths.min_absolute_access_count` | 10 | Minimum access count required for promotion candidacy | | `truths.min_total_memories` | 20 | Minimum memories required before distillation runs | | `truths.max_generation` | 5 | Maximum refinement generations before a Truth is protected | | `truths.auto_resolve_strength_threshold` | 15 | Evidence strength required for auto-resolution of conflicts | | `truths.similarity_threshold` | 0.40 | Minimum relevance score for a Truth to be included | | `truths.duplicate_threshold` | 0.80 | Similarity threshold to detect duplicate Truths | | `truths.stale_days` | 90 | Days without access before a Truth is considered stale | ## Managing Truths ### Via the UI The Truths page lets you: * View all your Truths with their source, generation, and access statistics * Create new Truths manually * Edit existing Truth content * Pin/unpin Truths * Delete Truths you no longer want * View Truth history and source memories The Conflicts page lets you: * Review flagged conflicts * Accept, reject, or merge proposed changes * See conflict resolution history ### Via Conversation Iris has tools to manage Truths during natural conversation: * **store\_truth** - Create a new Truth when you tell Iris something important * **search\_truths** - Find existing Truths * **update\_truth** - Modify a Truth's content * **delete\_truth** - Remove a Truth You can say things like "Remember that I'm allergic to shellfish - that's important" and Iris will create a Truth rather than a regular memory. ## Running Distillation Manually You can trigger distillation via Artisan: ```bash # Process all users php artisan iris:distill-truths --queue # Process a specific user php artisan iris:distill-truths --user=1 --queue # Preview candidates without promoting (dry run) php artisan iris:distill-truths --dry-run # View distillation statistics php artisan iris:distill-truths --user=1 --stats ``` The `--queue` flag dispatches jobs for background processing with automatic retry handling for rate limits. --- --- url: /index.md --- --- --- url: /integrations/weather.md --- # Weather Integration Iris integrates with [tomorrow.io](https://www.tomorrow.io/) to give her environmental awareness. Current conditions, short-term precipitation, and air quality automatically appear in every conversation — so when you mention outdoor plans, Iris already knows whether it's raining. ## Features * **Ambient context**: Current conditions, feels-like temperature, humidity, wind, and air quality injected into every conversation automatically * **Precipitation outlook**: Hourly precipitation probability for the next 4 hours * **Daily range**: Today's forecast high and low * **On-demand weather tool**: Query detailed forecasts (hourly up to 120h, daily up to 5 days) or real-time conditions for any location * **Air quality**: EPA AQI headline in ambient context; full breakdown (index value, primary pollutant) available via the weather tool * **Unit preferences**: Imperial (°F, mph) or Metric (°C, m/s) per user ## How It Works Tomorrow.io's forecast API is called once and cached using stale-while-revalidate. The resulting context is injected into Iris's system prompt alongside calendar context — so she can connect "you have a 2pm outdoor meeting" with "rain starts at 1:30pm" without being asked. Caching windows: * **Ambient context**: 30-minute fresh window, 60-minute stale window * **On-demand tool queries**: 5-minute fresh, 15-minute stale * Weather data for your configured location is shared between the ambient context and the tool — asking "what's the weather?" after Iris already loaded context doesn't trigger a second API call When no location is configured, the weather section is silently omitted from the system prompt. No errors, no placeholders. ## Setup ### 1. Get a Tomorrow.io API Key 1. Go to [tomorrow.io](https://www.tomorrow.io/) and create a free account 2. From your dashboard, navigate to **Development → API Keys** 3. Create a new key and copy it > \[!TIP] > The free tier covers real-time conditions, hourly forecasts (up to 120 hours), and daily forecasts (up to 5 days) — everything Iris needs. ### 2. Configure Environment Add these to your `.env`: ```bash IRIS_WEATHER_ENABLED=true TOMORROW_IO_API_KEY=your-api-key-here ``` ### 3. Set Your Location 1. Open Iris and go to **Settings → Weather** 2. Enter your location — accepts city name, US zip code, or lat/lon coordinates: * `Orlando, FL` * `32801` * `28.54,-81.38` 3. Choose your preferred units (Imperial or Metric) 4. Click **Save** Iris validates your location against tomorrow.io on save and shows the matched name so you can confirm the right place was resolved. ## Ambient Context Once configured, the weather section appears in Iris's system prompt automatically: ``` ## Current Weather (Orlando, FL) Partly Cloudy, 72.0°F (feels like 75.0°F) Humidity: 65% | Wind: 8.0 mph Air Quality: Good Today: High 78.0°F / Low 62.0°F ### Next 4 Hours - 1:00 PM: 20% chance of precipitation - 2:00 PM: 45% chance of precipitation - 3:00 PM: 80% chance of precipitation - 4:00 PM: 60% chance of precipitation ``` Iris uses this context to make weather-relevant observations without being prompted: > **You**: "Should I take the kids to the park this afternoon?" > > **Iris**: "Rain is likely by 3pm with an 80% chance — if you go, aim for before 2pm to beat it." ## Weather Tool Iris can look up weather for any location on demand using the `get_weather` tool. | Parameter | Description | |-----------|-------------| | `location` | City name, zip code, or lat/lon coordinates. Omit to use your configured default. | | `forecast_type` | `realtime` (precise current conditions), `hourly` (next 120 hours), or `daily` (next 5 days). Default: `hourly`. | Units always come from your user settings — there's no way to request a different unit system per query. Example queries Iris can handle with this tool: * "What's the weather in Chicago this weekend?" * "Is it going to rain in Denver tomorrow?" * "What's the air quality right now?" (uses your configured location) * "Give me a 5-day forecast for Miami" ## Air Quality Air quality context is included because both the AQI and its health concern level are directly relevant to outdoor activity planning. The ambient context shows a headline: ``` Air Quality: Unhealthy for Sensitive Groups ``` For more detail — the EPA index value, primary pollutant — ask Iris to look it up with the weather tool: > **You**: "What's the full air quality breakdown for today?" > > **Iris**: *(calls `get_weather` with `forecast_type: realtime`)* > > **Iris**: "Air quality is currently at EPA index 2 — Unhealthy for Sensitive Groups. The primary pollutant is PM2.5. I'd hold off on outdoor exercise today." ## Configuration | Environment Variable | Default | Description | |---------------------|---------|-------------| | `IRIS_WEATHER_ENABLED` | `false` | Enable weather integration | | `TOMORROW_IO_API_KEY` | — | Your tomorrow.io API key | The `iris.weather` config block in `config/iris.php` exposes additional tuning options: | Setting | Default | Description | |---------|---------|-------------| | `cache_ttl` | `30` | Fresh window for ambient context cache (minutes) | | `stale_ttl` | `60` | Stale window for ambient context cache (minutes) | | `tool_cache_ttl` | `5` | Fresh window for on-demand tool queries (minutes) | | `tool_stale_ttl` | `15` | Stale window for on-demand tool queries (minutes) | | `http_timeout` | `10` | Max seconds to wait for API response | | `http_connect_timeout` | `5` | Max seconds to establish TCP connection | ### User Settings Each user controls their own weather context from **Settings → Weather**: | Setting | Description | |---------|-------------| | **Location** | City name, zip code, or lat/lon. Validated against tomorrow.io on save. | | **Units** | Imperial (°F, mph) or Metric (°C, m/s). | Changing the location immediately invalidates the cache so the next conversation fetches fresh data for the new location. ## Disabling Weather * **System-wide**: Set `IRIS_WEATHER_ENABLED=false` in your `.env`. The Settings → Weather page shows a "not available" message and the nav item is hidden. No API calls are made. * **Per-user**: Clear the location field in Settings → Weather. The weather section is silently omitted from the system prompt when no location is set. --- --- url: /integrations/web-tools.md --- # Web Tools Iris has access to Anthropic's built-in web tools, enabling real-time web search and content fetching during conversations. These tools run on Anthropic's infrastructure, not your server. ## Available Tools ### Web Search Search the web for current information. Powered by Anthropic's search infrastructure. **Use cases:** * Finding recent news and events * Researching current topics * Looking up documentation * Fact-checking information ### Web Fetch Retrieve content from specific URLs. Fetches and parses web pages. **Use cases:** * Reading article content * Fetching documentation pages * Accessing public APIs * Retrieving linked resources ## Example Conversations ### Web Search > **You**: "What's the latest news about Laravel?" > > **Iris**: *\[searches the web]* > > "Here's what I found: Laravel 12 was released with improved performance, new Livewire 4 features announced at Laracon, Laravel Cloud entering public beta..." ### Web Fetch > **You**: "Can you check what the Prism documentation says about streaming?" > > **Iris**: *\[fetches the docs]* > > "According to the Prism documentation, streaming supports three response types..." ### Combined Usage > **You**: "Find best practices for Laravel API versioning and summarize them" > > **Iris**: *\[searches, then fetches relevant articles]* > > "Based on my research, here are the recommended approaches..." ## When Iris Uses Web Tools ### Uses web tools when: * Asked about current events or recent news * Questions require up-to-date information beyond training data * You provide a URL to read * Information isn't in memories or conversation context * Explicitly asked to search or look something up ### Avoids web tools when: * Information is available from memory * The question is about personal matters * Historical or stable information is sufficient * The answer is in the current conversation * You're discussing something Iris already knows about you > \[!NOTE] > Iris tries to be efficient with web tools. If it can answer from memory or context, it won't search unnecessarily. ## Privacy and Data Considerations ### What Gets Sent to Anthropic When web tools are used: * **Web search**: Your search query is sent to Anthropic's search infrastructure * **Web fetch**: The URL is sent to Anthropic's servers to fetch The fetched content is processed by Claude to generate a response. This happens on Anthropic's infrastructure, not your server. ### What Stays Local * Your conversation history * Your memories * Your calendar data * Your user profile Web tools don't have access to your local data. They only see the specific query or URL being processed. ### Disabling Web Tools for Privacy If you prefer Iris not to access the web: ```php // config/iris-custom.php return [ 'provider_tools' => [], ]; ``` Iris will rely solely on memories, conversation context, and its training data. ## Tool Invocation Behavior ### When Tools Are Called vs Skipped Iris decides whether to use web tools based on the request. The decision factors include: | Factor | More likely to search | Less likely to search | |--------|----------------------|----------------------| | Topic | Current events, recent releases | Personal preferences, historical facts | | Keywords | "latest", "recent", "news", "current" | "remember", "you know that", "I told you" | | Context | No relevant memories | Strong memory matches | | Explicitness | "Search for...", "Look up..." | General questions | ### Multiple Tool Calls Iris can chain web tools: 1. Search for relevant sources 2. Fetch the most promising result 3. Synthesize information from the fetched content This might look like: ``` web_search("Laravel 12 upgrade guide") → finds URLs web_fetch("https://laravel.com/docs/12.x/upgrade") → gets content → Summarizes the upgrade steps ``` ## Configuration Web tools are configured in `config/iris.php` under the `provider_tools` key. To customize without modifying core files, create `config/iris-custom.php`. ### Disabling All Web Tools ```php // config/iris-custom.php return [ 'provider_tools' => [], ]; ``` ### Disabling Specific Tools Keep only the tools you want: ```php // config/iris-custom.php return [ 'provider_tools' => [ ['type' => 'web_search_20250305', 'name' => 'web_search'], // web_fetch disabled ], ]; ``` Or disable just web search: ```php // config/iris-custom.php return [ 'provider_tools' => [ ['type' => 'web_fetch_20250910', 'name' => 'web_fetch'], // web_search disabled ], ]; ``` The `provider_tools` key uses a **replace** strategy -your list completely replaces the default. See [Customization](/advanced/customization) for more on local configuration. ## Best Practices **Be specific**: "Search for Laravel 11 upgrade guide from official docs" works better than "Find Laravel stuff" **Provide URLs when available**: "Read this article: https://example.com/article" is more reliable than searching **Ask for sources**: "What are PHP 8.4 features? Please cite sources." - Iris will include URLs **Specify recency**: "What are the latest PHP 8.4 features released this month?" helps get current results ## Troubleshooting **Could not fetch URL**: The site may block automated access, require authentication, or use heavy JavaScript rendering. Try a different source or ask Iris to search for alternatives. **Results seem outdated**: Ask specifically for recent information ("from 2026") or try different search terms. Web search results depend on what Anthropic's search infrastructure finds. **Tool not being used**: If Iris isn't searching when you expect it to, be more explicit: "Search the web for..." or "Look up the latest..." **Slow responses**: Web tool calls add latency since they require external requests. This is normal for search/fetch operations.