diff --git a/README.md b/README.md index 2ee43eb..4d0eee3 100644 --- a/README.md +++ b/README.md @@ -2,64 +2,43 @@ ![AI SDK Tools](image.png) -Essential utilities that extend and improve the Vercel AI SDK experience. State management, debugging tools, and structured artifact streaming - everything you need to build production-ready AI applications beyond simple chat interfaces. +Essential utilities for building production-ready AI applications with Vercel AI SDK. State management, debugging, structured streaming, intelligent agents, and caching - everything you need beyond basic chat interfaces. ## Packages -### ๐Ÿ—„๏ธ [@ai-sdk-tools/store](./packages/store) -AI chat state that scales with your application. Eliminates prop drilling within your chat components, ensuring better performance and cleaner architecture. +### [@ai-sdk-tools/store](./packages/store) +AI chat state management that eliminates prop drilling. Clean architecture and better performance for chat components. ```bash npm i @ai-sdk-tools/store ``` -### ๐Ÿ”ง [@ai-sdk-tools/devtools](./packages/devtools) -Development tools for debugging AI applications. A development-only debugging tool that integrates directly into your codebase, just like react-query-devtools. +### [@ai-sdk-tools/devtools](./packages/devtools) +Development tools for debugging AI applications. Inspect tool calls, messages, and execution flow directly in your app. ```bash npm i @ai-sdk-tools/devtools ``` -### ๐Ÿ“ฆ [@ai-sdk-tools/artifacts](./packages/artifacts) -Advanced streaming interfaces for AI applications. Create structured, type-safe artifacts that stream real-time updates from AI tools to React components. Perfect for dashboards, analytics, documents, and interactive experiences beyond chat. +### [@ai-sdk-tools/artifacts](./packages/artifacts) +Stream structured, type-safe artifacts from AI tools to React components. Build dashboards, analytics, and interactive experiences beyond chat. ```bash npm i @ai-sdk-tools/artifacts @ai-sdk-tools/store ``` -## Quick Example - -Build advanced AI interfaces with structured streaming: - -```tsx -// Define an artifact -const BurnRate = artifact('burn-rate', z.object({ - title: z.string(), - data: z.array(z.object({ - month: z.string(), - burnRate: z.number() - })) -})); - -// Stream from AI tool -const analysis = BurnRate.stream({ title: 'Q4 Analysis' }); -await analysis.update({ data: [{ month: '2024-01', burnRate: 50000 }] }); -await analysis.complete({ title: 'Q4 Analysis Complete' }); - -// Consume in React -function Dashboard() { - const { data, status, progress } = useArtifact(BurnRate); - - return ( -
-

{data?.title}

- {status === 'loading' &&
Loading... {progress * 100}%
} - {data?.data.map(item => ( -
{item.month}: ${item.burnRate}
- ))} -
- ); -} +### [@ai-sdk-tools/agents](./packages/agents) +Multi-agent orchestration with automatic handoffs and routing. Build intelligent workflows with specialized agents for any AI provider. + +```bash +npm i @ai-sdk-tools/agents ai zod +``` + +### [@ai-sdk-tools/cache](./packages/cache) +Universal caching for AI SDK tools. Cache expensive operations with zero configuration - works with regular tools, streaming, and artifacts. + +```bash +npm i @ai-sdk-tools/cache ``` ## Getting Started @@ -74,4 +53,4 @@ Visit our [website](https://ai-sdk-tools.dev) to explore interactive demos and d ## License -MIT \ No newline at end of file +MIT diff --git a/apps/example/src/ai/README.md b/apps/example/src/ai/README.md deleted file mode 100644 index e3bab76..0000000 --- a/apps/example/src/ai/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# AI Module Structure - -This directory contains all AI-related functionality organized by type. - -## Directory Structure - -``` -src/ai/ -โ”œโ”€โ”€ artifacts/ # AI SDK Artifacts -โ”‚ โ”œโ”€โ”€ burn-rate.ts # Burn rate analysis artifact -โ”‚ โ””โ”€โ”€ index.ts # Export all artifacts -โ”œโ”€โ”€ tools/ # AI SDK Tools -โ”‚ โ”œโ”€โ”€ burn-rate.ts # Burn rate analysis tool -โ”‚ โ””โ”€โ”€ index.ts # Export all tools -โ”œโ”€โ”€ index.ts # Main export file -โ””โ”€โ”€ README.md # This file -``` - -## Artifacts (`/artifacts`) - -Artifacts define the data schemas and streaming behavior for AI-generated content. They are used to create interactive, real-time updates in the UI. - -### Example: Burn Rate Artifact - -```typescript -import { BurnRateArtifact } from "@/ai/artifacts"; - -// Use in components -const burnRateData = useArtifact(BurnRateArtifact); -``` - -## Tools (`/tools`) - -Tools define the AI SDK tool implementations that can be called by the AI model. They contain the business logic and return structured data. - -### Example: Burn Rate Tool - -```typescript -import { analyzeBurnRateTool } from "@/ai/tools"; - -// Use in API routes -const result = streamText({ - model: openai("gpt-4o"), - tools: { - analyzeBurnRate: analyzeBurnRateTool, - }, -}); -``` - -## Adding New AI Features - -1. **Create Artifact**: Define the data schema in `/artifacts/feature-name.ts` -2. **Create Tool**: Implement the tool logic in `/tools/feature-name.ts` -3. **Export**: Add exports to the respective `index.ts` files -4. **Use**: Import from `@/ai` in your components and API routes - -## Best Practices - -- Keep artifacts focused on data structure and streaming behavior -- Keep tools focused on business logic and AI integration -- Use descriptive names for both artifacts and tools -- Export everything through index files for clean imports -- Document complex logic and data structures diff --git a/apps/example/src/ai/agents/agents/index.ts b/apps/example/src/ai/agents/agents/index.ts deleted file mode 100644 index ec83424..0000000 --- a/apps/example/src/ai/agents/agents/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Financial Specialist Agents - * - * Domain-specific agents for financial operations - */ - -export { invoicesAgent } from "./invoices-agent"; -export { reportsAgent } from "./reports-agent"; -export { transactionsAgent } from "./transactions-agent"; - -// Future agents to add: -// - Customers Agent -// - Time Tracking Agent -// - Forecasting Agent -// - Accounts Agent -// - Operations Agent diff --git a/apps/example/src/ai/agents/agents/invoices-agent.ts b/apps/example/src/ai/agents/agents/invoices-agent.ts deleted file mode 100644 index fce0b86..0000000 --- a/apps/example/src/ai/agents/agents/invoices-agent.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { openai } from "@ai-sdk/openai"; -import { Agent } from "@ai-sdk-tools/agents"; -import { getInvoiceTool, listInvoicesTool } from "../tools/invoices"; - -/** - * Invoices Specialist Agent - * - * Handles invoice queries and management - */ -export const invoicesAgent = new Agent({ - name: "Invoices Specialist", - model: openai("gpt-4o-mini"), - instructions: `You are an invoices specialist. You help users: - - Find and filter invoices by customer, date, status, or search query - - Get details of specific invoices - - Understand invoice status and payment information - -Common invoice statuses: -- draft: Invoice is being prepared, not sent yet -- unpaid: Invoice has been sent but not paid -- paid: Invoice has been paid in full -- overdue: Invoice is past due date and unpaid -- canceled: Invoice has been canceled - -Be helpful in suggesting filters and helping users track their invoices. -When discussing payment status, be clear about due dates and amounts.`, - tools: { - listInvoices: listInvoicesTool, - getInvoice: getInvoiceTool, - }, - maxTurns: 5, -}); diff --git a/apps/example/src/ai/agents/agents/reports-agent.ts b/apps/example/src/ai/agents/agents/reports-agent.ts deleted file mode 100644 index 139cbf5..0000000 --- a/apps/example/src/ai/agents/agents/reports-agent.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { openai } from "@ai-sdk/openai"; -import { Agent } from "@ai-sdk-tools/agents"; -import { - burnRateMetricsTool, - profitLossTool, - revenueDashboardTool, - runwayMetricsTool, - spendingMetricsTool, -} from "../tools/reports"; - -/** - * Financial Reports Specialist Agent - * - * Handles all financial reporting and metrics analysis - */ -export const reportsAgent = new Agent({ - name: "Financial Reports Specialist", - model: openai("gpt-4o-mini"), - instructions: `You are a financial reports specialist. You help users access and understand: - - P&L (Profit & Loss) statements - - Runway calculations - - Revenue metrics and analysis - - Burn rate analysis - - Spending breakdowns and analysis - -You have access to these specific tools: -- revenueDashboard: Generate comprehensive revenue dashboard with charts and metrics -- profitMetrics: Get P&L (profit & loss) metrics -- runwayMetrics: Calculate runway (months of cash remaining) -- burnRateMetrics: Analyze burn rate and cash consumption -- spendingMetrics: Get spending analysis with category breakdowns - -If asked about data you don't have access to (like balance sheets or tax summaries), politely explain what you CAN provide instead. - -Provide clear, concise summaries of financial data. Always format dates properly and clarify currency when relevant. - -When users ask about date ranges, help them with common periods: -- "Q1 2024" = January 1 - March 31, 2024 -- "last quarter" = previous 3 months -- "this month" = current month -- "YTD" = year to date (Jan 1 to today) -- "2024" = January 1 - December 31, 2024 - -Always express monetary values with the appropriate currency symbol or code.`, - tools: { - revenueDashboard: revenueDashboardTool, - profitMetrics: profitLossTool, - runwayMetrics: runwayMetricsTool, - burnRateMetrics: burnRateMetricsTool, - spendingMetrics: spendingMetricsTool, - }, - maxTurns: 8, -}); diff --git a/apps/example/src/ai/agents/agents/transactions-agent.ts b/apps/example/src/ai/agents/agents/transactions-agent.ts deleted file mode 100644 index b2b92f0..0000000 --- a/apps/example/src/ai/agents/agents/transactions-agent.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { openai } from "@ai-sdk/openai"; -import { Agent } from "@ai-sdk-tools/agents"; -import { - getTransactionTool, - listTransactionsTool, -} from "../tools/transactions"; - -/** - * Transactions Specialist Agent - * - * Handles transaction queries and searches - */ -export const transactionsAgent = new Agent({ - name: "Transactions Specialist", - model: openai("gpt-4o-mini"), - instructions: `You are a transactions specialist. You help users: - - Find and filter transactions by date, type, category, status, or search query - - Get details of specific transactions - - Understand transaction patterns and data - -Be helpful in suggesting useful filters when users ask about transactions. - -Common transaction statuses: -- pending: Transaction is not yet finalized -- completed: Transaction is complete -- posted: Transaction is posted to account -- archived: Transaction is archived -- excluded: Transaction is excluded from reports - -Transaction types: -- income: Money coming in -- expense: Money going out - -Explain the data clearly and help users find what they're looking for.`, - tools: { - listTransactions: listTransactionsTool, - getTransaction: getTransactionTool, - }, - maxTurns: 5, -}); diff --git a/apps/example/src/ai/agents/analytics.ts b/apps/example/src/ai/agents/analytics.ts new file mode 100644 index 0000000..fbc9ef5 --- /dev/null +++ b/apps/example/src/ai/agents/analytics.ts @@ -0,0 +1,67 @@ +/** + * Analytics Specialist Agent + * + * Analytics & forecasting specialist with business intelligence tools + */ + +import { openai } from "@ai-sdk/openai"; +import { + businessHealthScoreTool, + cashFlowForecastTool, + cashFlowStressTestTool, +} from "../tools/analytics"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const analyticsAgent = createAgent({ + name: "analytics", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are an analytics & forecasting specialist with access to business intelligence tools for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use your tools to run analysis - NEVER ask user for data +2. Call tools IMMEDIATELY when asked for forecasts, health scores, or stress tests +3. Present analytics clearly with key insights highlighted +4. Answer ONLY what was asked - don't provide extra analysis unless requested + +TOOL SELECTION: +- "health" or "healthy" queries โ†’ Use businessHealth tool (gives consolidated score) +- "forecast" or "prediction" โ†’ Use cashFlowForecast tool +- "stress test" or "what if" โ†’ Use stressTest tool +- DO NOT call multiple detailed tools (revenue, P&L, etc.) - use businessHealth for overview + +PRESENTATION STYLE: +- Reference ${ctx.companyName} when providing insights +- Use clear trend labels (Increasing, Decreasing, Stable) +- Use clear status labels (Healthy, Warning, Critical) +- Include confidence levels when forecasting (e.g., "High confidence", "Moderate risk") +- End with 2-3 actionable focus areas (not a laundry list) +- Keep responses concise - quality over quantity + +${formatContextForLLM(ctx)}`, + tools: { + businessHealth: businessHealthScoreTool, + cashFlowForecast: cashFlowForecastTool, + stressTest: cashFlowStressTestTool, + }, + matchOn: [ + "forecast", + "prediction", + "predict", + "stress test", + "what if", + "scenario", + "health score", + "business health", + "healthy", + "health", + "analyze", + "analysis", + "future", + "projection", + /forecast/i, + /health.*score/i, + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/customers.ts b/apps/example/src/ai/agents/customers.ts new file mode 100644 index 0000000..50284d1 --- /dev/null +++ b/apps/example/src/ai/agents/customers.ts @@ -0,0 +1,36 @@ +import { openai } from "@ai-sdk/openai"; +import { + createCustomerTool, + customerProfitabilityTool, + getCustomerTool, + updateCustomerTool, +} from "../tools/customers"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const customersAgent = createAgent({ + name: "customers", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are a customer management specialist for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use tools to get/create/update customer data +2. Present customer information clearly with key details +3. Highlight profitability insights when analyzing customers + +${formatContextForLLM(ctx)}`, + tools: { + getCustomer: getCustomerTool, + createCustomer: createCustomerTool, + updateCustomer: updateCustomerTool, + profitabilityAnalysis: customerProfitabilityTool, + }, + matchOn: [ + "customer", + "client", + "customer profitability", + "customer analysis", + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/general.ts b/apps/example/src/ai/agents/general.ts new file mode 100644 index 0000000..fef8666 --- /dev/null +++ b/apps/example/src/ai/agents/general.ts @@ -0,0 +1,46 @@ +import { openai } from "@ai-sdk/openai"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const generalAgent = createAgent({ + name: "general", + model: openai("gpt-4o-mini"), + instructions: (ctx) => `You are a general assistant for ${ctx.companyName}. + +YOUR ROLE: +- Handle general conversation (greetings, thanks, casual chat) +- Answer questions about what you can do and your capabilities +- Handle ambiguous or unclear requests by asking clarifying questions +- Provide helpful information about the available specialists + +AVAILABLE SPECIALISTS: +- **reports**: Financial metrics (revenue, P&L, burn rate, runway, etc.) +- **transactions**: Transaction history and details +- **invoices**: Invoice management +- **timeTracking**: Time tracking and timers +- **customers**: Customer management and profitability +- **analytics**: Forecasting and business intelligence +- **operations**: Inbox, documents, balances, data export + +STYLE: +- Be friendly and helpful +- Reference ${ctx.companyName} when relevant +- If the user asks for something specific, suggest the right specialist +- Keep responses concise but complete + +${formatContextForLLM(ctx)}`, + matchOn: [ + "hello", + "hi", + "hey", + "thanks", + "thank you", + "what can you do", + "previous question", + "last question", + "help", + "how does this work", + "what are you", + "who are you", + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/index.ts b/apps/example/src/ai/agents/index.ts index 0f3865c..4cda103 100644 --- a/apps/example/src/ai/agents/index.ts +++ b/apps/example/src/ai/agents/index.ts @@ -1,17 +1,13 @@ -/** - * Financial Agents - Main Entry Point - * - * Modular agent system for comprehensive financial operations - */ +export { analyticsAgent } from "./analytics"; +export { customersAgent } from "./customers"; +export { generalAgent } from "./general"; +export { invoicesAgent } from "./invoices"; +export { operationsAgent } from "./operations"; +export { reportsAgent } from "./reports"; -// Individual specialist agents (for advanced usage) -export { - invoicesAgent, - reportsAgent, - transactionsAgent, -} from "./agents"; -// Main orchestrator (primary export) -export { orchestratorAgent } from "./orchestrator"; +// Shared utilities +export { timeTrackingAgent } from "./time-tracking"; +export { transactionsAgent } from "./transactions"; -// Re-export types and utilities for external use -export type * from "./types/filters"; +// Triage agent (main entry point) +export { triageAgent } from "./triage"; diff --git a/apps/example/src/ai/agents/invoices.ts b/apps/example/src/ai/agents/invoices.ts new file mode 100644 index 0000000..045e4dc --- /dev/null +++ b/apps/example/src/ai/agents/invoices.ts @@ -0,0 +1,39 @@ +import { openai } from "@ai-sdk/openai"; +import { + createInvoiceTool, + getInvoiceTool, + listInvoicesTool, + updateInvoiceTool, +} from "../tools/invoices"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const invoicesAgent = createAgent({ + name: "invoices", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are an invoice management specialist for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use tools to get/create/update invoice data +2. Present invoice information clearly with key details (amount, status, due date) +3. Use clear status labels (Paid, Overdue, Pending) + +${formatContextForLLM(ctx)}`, + tools: { + listInvoices: listInvoicesTool, + getInvoice: getInvoiceTool, + createInvoice: createInvoiceTool, + updateInvoice: updateInvoiceTool, + }, + matchOn: [ + "invoice", + "bill", + "create invoice", + "send invoice", + "unpaid invoice", + "paid invoice", + /create.*invoice/i, + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/operations.ts b/apps/example/src/ai/agents/operations.ts new file mode 100644 index 0000000..7ca2ac8 --- /dev/null +++ b/apps/example/src/ai/agents/operations.ts @@ -0,0 +1,31 @@ +import { openai } from "@ai-sdk/openai"; +import { + exportDataTool, + getBalancesTool, + listDocumentsTool, + listInboxItemsTool, +} from "../tools/operations"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const operationsAgent = createAgent({ + name: "operations", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are an operations specialist for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use tools to get inbox items, documents, balances, or export data +2. Present information clearly with counts and summaries +3. Organize multiple items in clear lists or tables + +${formatContextForLLM(ctx)}`, + tools: { + listInbox: listInboxItemsTool, + getBalances: getBalancesTool, + listDocuments: listDocumentsTool, + exportData: exportDataTool, + }, + matchOn: ["inbox", "document", "export", "balance", "account balance"], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/orchestrator.ts b/apps/example/src/ai/agents/orchestrator.ts deleted file mode 100644 index 403e8f8..0000000 --- a/apps/example/src/ai/agents/orchestrator.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { openai } from "@ai-sdk/openai"; -import { Agent } from "@ai-sdk-tools/agents"; -import { invoicesAgent, reportsAgent, transactionsAgent } from "./agents"; - -/** - * Financial Assistant Orchestrator - * - * Main entry point that routes user queries to appropriate specialist agents - */ -export const orchestratorAgent = Agent.create({ - name: "Financial Assistant", - model: openai("gpt-4o-mini"), - instructions: `You are a financial assistant that routes requests to specialist agents. - -**Financial Reports Specialist** handles: -- Revenue metrics -- Profit & Loss (P&L) -- Runway calculations -- Burn rate analysis -- Spending analysis - -**Transactions Specialist** handles: -- Listing transactions with filters -- Getting transaction details - -**Invoices Specialist** handles: -- Listing invoices with filters -- Getting invoice details - -Route to the appropriate specialist. If a query needs multiple specialists, route to the first one with context about what else is needed.`, - handoffs: [reportsAgent, transactionsAgent, invoicesAgent], - maxTurns: 3, - temperature: 0, -}); diff --git a/apps/example/src/ai/agents/reports.ts b/apps/example/src/ai/agents/reports.ts new file mode 100644 index 0000000..cd40517 --- /dev/null +++ b/apps/example/src/ai/agents/reports.ts @@ -0,0 +1,77 @@ +import { openai } from "@ai-sdk/openai"; +import { + balanceSheetTool, + burnRateMetricsTool, + cashFlowTool, + expensesTool, + profitLossTool, + revenueDashboardTool, + runwayMetricsTool, + spendingMetricsTool, + taxSummaryTool, +} from "../tools/reports"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const reportsAgent = createAgent({ + name: "reports", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are a financial reports specialist with access to live financial data. + +YOUR SCOPE: Provide specific financial reports (revenue, P&L, cash flow, etc.) +NOT YOUR SCOPE: Business health analysis, forecasting (those go to analytics specialist) + +CRITICAL RULES: +1. ALWAYS use your tools to get data - NEVER ask the user for information you can retrieve +2. Call tools IMMEDIATELY when asked for financial metrics +3. Present results clearly after retrieving data +4. For date ranges: "Q1 2024" = 2024-01-01 to 2024-03-31, "2024" = 2024-01-01 to 2024-12-31 +5. Answer ONLY what was asked - don't provide extra reports unless requested + +TOOL SELECTION GUIDE: +- "runway" or "how long can we last" โ†’ Use runway tool +- "burn rate" or "monthly burn" โ†’ Use burnRate tool +- "revenue" or "income" โ†’ Use revenue tool +- "P&L" or "profit" or "loss" โ†’ Use profitLoss tool +- "cash flow" โ†’ Use cashFlow tool +- "balance sheet" or "assets/liabilities" โ†’ Use balanceSheet tool +- "expenses" or "spending breakdown" โ†’ Use expenses tool +- "tax" โ†’ Use taxSummary tool + +PRESENTATION STYLE: +- Reference the company name (${ctx.companyName}) when providing insights +- Use clear sections with headers for multiple metrics +- Include status indicators (e.g., "Status: Healthy", "Warning", "Critical") +- End with a brief key insight or takeaway when relevant +- Be concise but complete - no unnecessary fluff + +${formatContextForLLM(ctx)}`, + tools: { + revenue: revenueDashboardTool, + profitLoss: profitLossTool, + cashFlow: cashFlowTool, + balanceSheet: balanceSheetTool, + expenses: expensesTool, + burnRate: burnRateMetricsTool, + runway: runwayMetricsTool, + spending: spendingMetricsTool, + taxSummary: taxSummaryTool, + }, + matchOn: [ + "revenue", + "profit", + "loss", + "p&l", + "runway", + "burn rate", + "expenses", + "spending", + "balance sheet", + "tax", + "financial report", + /burn.?rate/i, + /profit.*loss/i, + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/shared.ts b/apps/example/src/ai/agents/shared.ts new file mode 100644 index 0000000..417797b --- /dev/null +++ b/apps/example/src/ai/agents/shared.ts @@ -0,0 +1,83 @@ +/** + * Shared Agent Configuration + * + * Dynamic context and utilities used across all agents + */ + +import type { AgentConfig } from "@ai-sdk-tools/agents"; +import { Agent } from "@ai-sdk-tools/agents"; + +/** + * Application context passed to agents + * Built dynamically per-request with current date/time + */ +export interface AppContext { + userId: string; + fullName: string; + email: string; + teamId: string; + companyName: string; + baseCurrency: string; + locale: string; + currentDate: string; + currentTime: string; + currentDateTime: string; + timezone: string; + // Allow additional properties to satisfy Record constraint + [key: string]: unknown; +} + +/** + * Build application context dynamically + * Ensures current date/time on every request + */ +export function buildAppContext(params: { + userId: string; + fullName: string; + email: string; + teamId: string; + companyName: string; + baseCurrency?: string; + locale?: string; + timezone?: string; +}): AppContext { + const now = new Date(); + return { + userId: params.userId, + fullName: params.fullName, + email: params.email, + teamId: params.teamId, + companyName: params.companyName, + baseCurrency: params.baseCurrency || "USD", + locale: params.locale || "en-US", + currentDate: now.toISOString().split("T")[0], + currentTime: now.toTimeString().split(" ")[0], + currentDateTime: now.toISOString(), + timezone: + params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + }; +} + +/** + * Format context for LLM system prompts + * Auto-injected by agent instructions functions + */ +export function formatContextForLLM(context: AppContext): string { + return ` +CURRENT CONTEXT: +- Date: ${context.currentDate} ${context.currentTime} (${context.timezone}) +- User: ${context.fullName} (${context.email}) +- Company: ${context.companyName} +- Currency: ${context.baseCurrency} +- Locale: ${context.locale} + +Important: Use the current date/time above for any time-sensitive operations. +`; +} + +/** + * Create a typed agent with AppContext pre-applied + * This enables automatic type inference for the context parameter + */ +export const createAgent = (config: AgentConfig) => + Agent.create(config); diff --git a/apps/example/src/ai/agents/time-tracking.ts b/apps/example/src/ai/agents/time-tracking.ts new file mode 100644 index 0000000..9f1f8e0 --- /dev/null +++ b/apps/example/src/ai/agents/time-tracking.ts @@ -0,0 +1,45 @@ +import { openai } from "@ai-sdk/openai"; +import { + createTimeEntryTool, + deleteTimeEntryTool, + getTimeEntriesTool, + getTrackerProjectsTool, + startTimerTool, + stopTimerTool, + updateTimeEntryTool, +} from "../tools/tracker"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const timeTrackingAgent = createAgent({ + name: "timeTracking", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are a time tracking specialist for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use tools to get/create/update time entries and timers +2. Present time data clearly (duration, project, date) +3. Summarize totals when showing multiple entries + +${formatContextForLLM(ctx)}`, + tools: { + startTimer: startTimerTool, + stopTimer: stopTimerTool, + getTimeEntries: getTimeEntriesTool, + createTimeEntry: createTimeEntryTool, + updateTimeEntry: updateTimeEntryTool, + deleteTimeEntry: deleteTimeEntryTool, + getProjects: getTrackerProjectsTool, + }, + matchOn: [ + "timer", + "time entry", + "time tracking", + "hours", + "tracked time", + "start timer", + "stop timer", + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/transactions.ts b/apps/example/src/ai/agents/transactions.ts new file mode 100644 index 0000000..862c784 --- /dev/null +++ b/apps/example/src/ai/agents/transactions.ts @@ -0,0 +1,42 @@ +import { openai } from "@ai-sdk/openai"; +import { + getTransactionTool, + listTransactionsTool, +} from "../tools/transactions"; +import { createAgent, formatContextForLLM } from "./shared"; + +export const transactionsAgent = createAgent({ + name: "transactions", + model: openai("gpt-4o-mini"), + instructions: ( + ctx, + ) => `You are a transactions specialist with access to live transaction data for ${ctx.companyName}. + +CRITICAL RULES: +1. ALWAYS use your tools to get data - NEVER ask the user for transaction details +2. Call tools IMMEDIATELY when asked about transactions +3. For "largest transactions", use sort and limit filters +4. Present transaction data clearly in tables or lists + +PRESENTATION STYLE: +- Reference ${ctx.companyName} when relevant +- Use clear formatting (tables/lists) for multiple transactions +- Highlight key insights (e.g., "Largest expense: Marketing at 5,000 SEK") +- Be concise and data-focused + +${formatContextForLLM(ctx)}`, + tools: { + listTransactions: listTransactionsTool, + getTransaction: getTransactionTool, + }, + matchOn: [ + "transaction", + "payment", + "transfer", + "purchase", + "last transaction", + "recent transaction", + "latest transaction", + ], + maxTurns: 5, +}); diff --git a/apps/example/src/ai/agents/triage.ts b/apps/example/src/ai/agents/triage.ts new file mode 100644 index 0000000..42369eb --- /dev/null +++ b/apps/example/src/ai/agents/triage.ts @@ -0,0 +1,69 @@ +import { openai } from "@ai-sdk/openai"; +import { analyticsAgent } from "./analytics"; +import { customersAgent } from "./customers"; +import { generalAgent } from "./general"; +import { invoicesAgent } from "./invoices"; +import { operationsAgent } from "./operations"; +import { reportsAgent } from "./reports"; +import { createAgent, formatContextForLLM } from "./shared"; +import { timeTrackingAgent } from "./time-tracking"; +import { transactionsAgent } from "./transactions"; + +export const triageAgent = createAgent({ + name: "triage", + model: openai("gpt-4o-mini"), + instructions: (ctx) => `Route user requests to the appropriate agent: + +**reports**: Financial metrics and reports + - Revenue, P&L, expenses, spending + - Burn rate, runway (how long money will last) + - Cash flow, balance sheet, tax summary + +**transactions**: Transaction queries + - List transactions, search transactions + - Get specific transaction details + +**invoices**: Invoice management + - Create, update, list invoices + +**timeTracking**: Time tracking + - Start/stop timers, time entries + +**customers**: Customer management + - Get/create/update customers, profitability analysis + +**analytics**: Advanced forecasting & analysis + - Business health score + - Cash flow forecasting (future predictions) + - Stress testing scenarios + +**operations**: Operations + - Inbox, balances, documents, exports + +**general**: General queries and conversation + - Greetings, thanks, casual conversation + - "What can you do?", "How does this work?" + - Memory queries: "What did I just ask?", "What did we discuss?" + - Ambiguous or unclear requests + - Default for anything that doesn't fit other specialists + +ROUTING RULES: +- "runway" = reports (not analytics) +- "forecast" = analytics (not reports) +- "what did I just ask" or memory queries = general +- Greetings, thanks, casual chat = general +- When uncertain = general (as default) +- Route to ONE specialist at a time + +${formatContextForLLM(ctx)}`, + handoffs: [ + reportsAgent, + analyticsAgent, + transactionsAgent, + invoicesAgent, + timeTrackingAgent, + customersAgent, + operationsAgent, + generalAgent, + ], +}); diff --git a/apps/example/src/ai/agents/utils/README.md b/apps/example/src/ai/agents/utils/README.md deleted file mode 100644 index 928d90d..0000000 --- a/apps/example/src/ai/agents/utils/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# Utilities - -Shared utility functions used across the agents system. - -## ๐Ÿ“ Files - -### `date-helpers.ts` -Date and time utilities for financial operations: -- `getStartOfMonth()` / `getEndOfMonth()` -- `getStartOfYear()` / `getEndOfYear()` -- `getDaysAgo(days)` - Get date N days ago -- `formatDateRange(from, to)` - Format display -- `isValidISODate(dateString)` - Validation - -### `filter-builders.ts` -Helper functions for building filter descriptions: -- `buildFilterDescription(filters)` - Human-readable filter text -- `cleanFilters(filters)` - Remove undefined/null values - -### `fake-data.ts` โœจ -Realistic fake data generators using `@faker-js/faker`: - -#### Financial Reports -- `generateRevenueMetrics(params)` - Revenue analysis -- `generateProfitLossMetrics(params)` - P&L statements -- `generateRunwayMetrics(params)` - Runway calculations -- `generateBurnRateMetrics(params)` - Burn rate tracking -- `generateSpendingMetrics(params)` - Spending breakdown - -#### Transactions -- `generateTransactions(params)` - List of transactions -- `generateTransaction(id)` - Single transaction details - -#### Invoices -- `generateInvoices(params)` - List of invoices -- `generateInvoice(id)` - Single invoice details - -## ๐ŸŽฒ Fake Data Features - -### Realistic Financial Data -- **Proper calculations:** Revenue = profit + expenses -- **Realistic ranges:** Amounts between $50-$500K -- **Trends:** Growth percentages, burn rate efficiency -- **Breakdowns:** Categories, merchants, tags - -### Smart Defaults -- Currency defaults to USD -- Dates respect provided ranges -- Page sizes honor limits -- Status distribution matches reality - -### Consistent Structures -All fake data matches expected schemas: -- Proper date formats (ISO 8601) -- Complete customer objects -- Detailed line items -- Realistic metadata - -## ๐Ÿ’ก Usage Examples - -### Generate Revenue Metrics -```typescript -import { generateRevenueMetrics } from './fake-data'; - -const data = generateRevenueMetrics({ - from: '2024-01-01', - to: '2024-03-31', - currency: 'USD' -}); -// Returns: { period, currency, total, breakdown, growth } -``` - -### Generate Transactions -```typescript -import { generateTransactions } from './fake-data'; - -const data = generateTransactions({ - pageSize: 20, - start: '2024-01-01', - end: '2024-12-31', - type: 'expense' -}); -// Returns: { data: [...], pagination: {...} } -``` - -### Generate Invoice -```typescript -import { generateInvoice } from './fake-data'; - -const invoice = generateInvoice('INV-1234'); -// Returns complete invoice with customer, line items, totals -``` - -## ๐Ÿ”„ Replacing with Real Data - -When you're ready to use real database data, simply replace the fake data generators in the tool files: - -```typescript -// Before (fake data): -import { generateRevenueMetrics } from '../../utils/fake-data'; -execute: async ({ from, to, currency }) => { - return generateRevenueMetrics({ from, to, currency }); -} - -// After (real data): -import { db } from '@/lib/database'; -execute: async ({ from, to, currency }) => { - return await db.getRevenueMetrics({ from, to, currency }); -} -``` - -## ๐ŸŽจ Customizing Fake Data - -To customize the fake data: - -1. **Edit generators** in `fake-data.ts` -2. **Adjust ranges** - Change min/max values -3. **Add fields** - Include additional data -4. **Change distributions** - Modify status/type probabilities - -Example: -```typescript -// Increase revenue range -const totalRevenue = faker.number.float({ - min: 100000, // was 50000 - max: 1000000, // was 500000 - fractionDigits: 2, -}); -``` - -## ๐Ÿงช Testing - -Fake data is perfect for: -- โœ… Development without database -- โœ… Testing agent behavior -- โœ… Demos and presentations -- โœ… Integration tests -- โœ… UI component development - -## ๐Ÿ“ฆ Dependencies - -- `@faker-js/faker` (v10+) - Fake data generation -- No other external dependencies - ---- - -**Status:** โœ… Complete -**Generators:** 9 financial data generators -**Coverage:** Reports, Transactions, Invoices - diff --git a/apps/example/src/ai/context.ts b/apps/example/src/ai/context.ts deleted file mode 100644 index 39d02b8..0000000 --- a/apps/example/src/ai/context.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type BaseContext, createTypedContext } from "@ai-sdk-tools/artifacts"; - -// Define custom context type with database and user info (mirrors real app) -interface ChatContext extends BaseContext { - userId: string; - fullName: string; - db: any; // Mock database - user: { - teamId: string; - baseCurrency: string; - locale: string; - fullName: string; - }; -} - -// Create typed context helpers -const { setContext, getContext } = createTypedContext(); - -// Helper function to get current user context (can be used in tools) -export function getCurrentUser() { - const context = getContext(); - return { - id: context.userId, - fullName: context.fullName, - }; -} - -export { setContext, getContext }; diff --git a/apps/example/src/ai/index.ts b/apps/example/src/ai/index.ts index b309da0..6448040 100644 --- a/apps/example/src/ai/index.ts +++ b/apps/example/src/ai/index.ts @@ -1,2 +1,11 @@ +// Agents +export * from "./agents"; +// Artifacts export * from "./artifacts"; -export * from "./tools"; + +// Types +export * from "./types/filters"; + +// Utils +export * from "./utils/date-helpers"; +export * from "./utils/filter-builders"; diff --git a/apps/example/src/ai/agents/tools/analytics/business-health-score.ts b/apps/example/src/ai/tools/analytics/business-health-score.ts similarity index 88% rename from apps/example/src/ai/agents/tools/analytics/business-health-score.ts rename to apps/example/src/ai/tools/analytics/business-health-score.ts index be609c3..d9c986c 100644 --- a/apps/example/src/ai/agents/tools/analytics/business-health-score.ts +++ b/apps/example/src/ai/tools/analytics/business-health-score.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateBusinessHealthScore } from "../../utils/fake-data"; +import { generateBusinessHealthScore } from "@/ai/utils/fake-data"; export const businessHealthScoreTool = tool({ description: `Calculate comprehensive business health score based on key metrics. diff --git a/apps/example/src/ai/agents/tools/analytics/cash-flow-forecast.ts b/apps/example/src/ai/tools/analytics/cash-flow-forecast.ts similarity index 85% rename from apps/example/src/ai/agents/tools/analytics/cash-flow-forecast.ts rename to apps/example/src/ai/tools/analytics/cash-flow-forecast.ts index f3a5f69..c0b7cb0 100644 --- a/apps/example/src/ai/agents/tools/analytics/cash-flow-forecast.ts +++ b/apps/example/src/ai/tools/analytics/cash-flow-forecast.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { currencyFilterSchema } from "../../types/filters"; -import { generateCashFlowForecast } from "../../utils/fake-data"; +import { currencyFilterSchema } from "@/ai/types/filters"; +import { generateCashFlowForecast } from "@/ai/utils/fake-data"; export const cashFlowForecastTool = tool({ description: `Forecast future cash flow based on historical data and unpaid invoices. diff --git a/apps/example/src/ai/agents/tools/analytics/cash-flow-stress-test.ts b/apps/example/src/ai/tools/analytics/cash-flow-stress-test.ts similarity index 91% rename from apps/example/src/ai/agents/tools/analytics/cash-flow-stress-test.ts rename to apps/example/src/ai/tools/analytics/cash-flow-stress-test.ts index 841da39..ee46984 100644 --- a/apps/example/src/ai/agents/tools/analytics/cash-flow-stress-test.ts +++ b/apps/example/src/ai/tools/analytics/cash-flow-stress-test.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateCashFlowStressTest } from "../../utils/fake-data"; +import { generateCashFlowStressTest } from "@/ai/utils/fake-data"; export const cashFlowStressTestTool = tool({ description: `Perform cash flow stress testing under various scenarios. diff --git a/apps/example/src/ai/agents/tools/analytics/index.ts b/apps/example/src/ai/tools/analytics/index.ts similarity index 63% rename from apps/example/src/ai/agents/tools/analytics/index.ts rename to apps/example/src/ai/tools/analytics/index.ts index 41d75e3..22230b4 100644 --- a/apps/example/src/ai/agents/tools/analytics/index.ts +++ b/apps/example/src/ai/tools/analytics/index.ts @@ -1,9 +1,3 @@ -/** - * Analytics & Forecasting Tools - * - * Advanced analytics, forecasting, and business intelligence tools - */ - export { businessHealthScoreTool } from "./business-health-score"; export { cashFlowForecastTool } from "./cash-flow-forecast"; export { cashFlowStressTestTool } from "./cash-flow-stress-test"; diff --git a/apps/example/src/ai/tools/burn-rate.ts b/apps/example/src/ai/tools/burn-rate.ts deleted file mode 100644 index 02975e4..0000000 --- a/apps/example/src/ai/tools/burn-rate.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { tool } from "ai"; -import { z } from "zod"; -import { cachedUpstash } from "@/lib/cache"; -import { BurnRateArtifact } from "@/ai/artifacts/burn-rate"; -import { delay } from "@/lib/delay"; - -export const analyzeBurnRateTool = tool({ - description: - "Analyze company burn rate with interactive chart data and insights. Use this when users ask about burn rate analysis, financial health, runway calculations, or expense tracking.", - inputSchema: z.object({ - companyName: z.string().describe("Name of the company to analyze"), - monthlyData: z - .array( - z.object({ - month: z.string().describe("Month in YYYY-MM format"), - revenue: z.number().describe("Monthly revenue"), - expenses: z.number().describe("Monthly expenses"), - cashBalance: z.number().describe("Cash balance at end of month"), - }), - ) - .describe("Array of monthly financial data"), - }), - execute: async function* ({ companyName, monthlyData }) { - const analysis = BurnRateArtifact.stream({ - stage: "loading", - title: `${companyName} Burn Rate Analysis`, - chartData: [], - progress: 0, - }); - - // Yield initial progress - yield { text: `Starting burn rate analysis for ${companyName}...` }; - - await delay(500); - - // Step 2: Processing - generate chart data - analysis.progress = 0.1; - await analysis.update({ stage: "processing" }); - - yield { text: `Processing financial data...` }; - - for (const [index, month] of monthlyData.entries()) { - const burnRate = month.expenses - month.revenue; - const runway = burnRate > 0 ? month.cashBalance / burnRate : Infinity; - - await analysis.update({ - chartData: [ - ...analysis.data.chartData, - { - month: month.month, - revenue: month.revenue, - expenses: month.expenses, - burnRate, - runway: Math.min(runway, 24), // Cap at 24 months for display - }, - ], - progress: ((index + 1) / monthlyData.length) * 0.7, // 70% for data processing - }); - - await delay(200); // Simulate processing time - } - - await delay(300); - - // Step 3: Analyzing - generate insights - await analysis.update({ stage: "analyzing" }); - analysis.progress = 0.9; - - const avgBurnRate = - analysis.data.chartData.reduce((sum, d) => sum + d.burnRate, 0) / - analysis.data.chartData.length; - const avgRunway = - analysis.data.chartData.reduce((sum, d) => sum + d.runway, 0) / - analysis.data.chartData.length; - - // Determine trend - const firstHalf = analysis.data.chartData.slice( - 0, - Math.floor(analysis.data.chartData.length / 2), - ); - const secondHalf = analysis.data.chartData.slice( - Math.floor(analysis.data.chartData.length / 2), - ); - const firstAvg = - firstHalf.reduce((sum, d) => sum + d.burnRate, 0) / firstHalf.length; - const secondAvg = - secondHalf.reduce((sum, d) => sum + d.burnRate, 0) / secondHalf.length; - - const trend = - secondAvg < firstAvg - ? ("improving" as const) - : secondAvg > firstAvg - ? ("declining" as const) - : ("stable" as const); - - // Generate alerts and recommendations - const alerts: string[] = []; - const recommendations: string[] = []; - - if (avgRunway < 6) { - alerts.push("Critical: Average runway below 6 months"); - recommendations.push("Consider immediate cost reduction or fundraising"); - } else if (avgRunway < 12) { - alerts.push("Warning: Average runway below 12 months"); - recommendations.push("Plan fundraising or revenue optimization"); - } - - if (trend === "declining") { - alerts.push("Burn rate trend is worsening"); - recommendations.push( - "Review expense categories for optimization opportunities", - ); - } - - if (avgBurnRate < 0) { - recommendations.push("Great! You're generating positive cash flow"); - } - - await delay(400); - - // Step 4: Complete with summary (including user context) - const finalData = { - title: `${companyName} Burn Rate Analysis`, - stage: "complete" as const, - currency: "USD", - chartData: analysis.data.chartData, - progress: 1, - summary: { - currentBurnRate: avgBurnRate, - averageRunway: avgRunway, - trend, - alerts, - recommendations, - }, - }; - - // Complete the artifact with final data - this should call artifacts.getWriter() - await analysis.complete(finalData); - - // Final yield with completion - yield { - text: `Completed burn rate analysis for ${companyName}. Average burn rate: $${avgBurnRate.toLocaleString()}/month. Runway: ${avgRunway.toFixed(1)} months. Trend: ${trend}.`, - forceStop: true, - }; - }, -}); - -// Create cached version - uses pre-configured cache -export const cachedAnalyzeBurnRateTool = cachedUpstash(analyzeBurnRateTool); diff --git a/apps/example/src/ai/tools/complex-burn-rate.ts b/apps/example/src/ai/tools/complex-burn-rate.ts deleted file mode 100644 index 1e28d9f..0000000 --- a/apps/example/src/ai/tools/complex-burn-rate.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { openai } from "@ai-sdk/openai"; -import { cachedUpstash } from "@/lib/cache"; -import { getBurnRate, getRunway, getSpending } from "@/lib/mock-db-queries"; -import { formatAmount, safeValue, generateFollowupQuestions } from "@/lib/mock-utils"; -import { generateText, smoothStream, streamText, tool } from "ai"; -import { - eachMonthOfInterval, - endOfMonth, - format, - startOfMonth, -} from "date-fns"; -import { BurnRateArtifact } from "@/ai/artifacts/burn-rate"; -import { followupQuestionsArtifact } from "@/ai/artifacts/followup-questions"; -import { getContext } from "@/ai/context"; -import { getBurnRateSchema } from "./schema"; - -export const complexBurnRateAnalysis = tool({ - description: - "Generate comprehensive burn rate analysis with interactive visualizations, spending trends, runway projections, and actionable insights. This mirrors the real-world tool to test complex streaming and caching patterns.", - inputSchema: getBurnRateSchema.omit({ showCanvas: true }), - execute: async function* ({ from, to, currency }) { - try { - const context = getContext(); - - // Always create canvas for analysis tool - const analysis = BurnRateArtifact.stream({ - stage: "loading", - title: "Complex Burn Rate Analysis", - currency: currency ?? context.user.baseCurrency ?? "USD", - chartData: [], - progress: 0, - }); - - // Generate a contextual initial message based on the analysis request - const initialMessageStream = streamText({ - model: openai("gpt-4o-mini"), - temperature: 0.2, - system: `You are a financial assistant generating a brief initial message for a burn rate analysis. - -The user has requested a burn rate analysis for the period ${from} to ${to}. Create a message that: -- Acknowledges the specific time period being analyzed -- Explains what you're currently doing (gathering financial data) -- Mentions the specific insights they'll receive (monthly burn rate, cash runway, expense breakdown) -- Uses a warm, personal tone while staying professional -- Uses the user's first name (${safeValue(context?.user.fullName?.split(" ")[0]) || "there"}) when appropriate -- Shows genuine interest in their financial well-being -- Avoids generic phrases like "Got it! Let's dive into..." or "Thanks for reaching out" -- Keep it concise (1-2 sentences max) - -Example format: "I'm analyzing your burn rate data for [period] to show your monthly spending patterns, cash runway, and expense breakdown."`, - messages: [ - { - role: "user", - content: `Generate a brief initial message for a burn rate analysis request for the period ${from} to ${to}.`, - }, - ], - experimental_transform: smoothStream({ chunking: "word" }), - }); - - let completeMessage = ""; - for await (const chunk of initialMessageStream.textStream) { - completeMessage += chunk; - // Yield the accumulated text so far for streaming effect - yield { text: completeMessage }; - } - - // Add line breaks to prepare for the detailed analysis - completeMessage += "\n"; - - // Yield to continue processing while showing loading step - yield { text: completeMessage }; - - // Run all database queries in parallel for maximum performance - const [burnRateData, runway, spendingData] = await Promise.all([ - getBurnRate(context.db, { - teamId: context.user.teamId, - from, - to, - currency: currency ?? undefined, - }), - getRunway(context.db, { - teamId: context.user.teamId, - from, - to, - currency: currency ?? undefined, - }), - getSpending(context.db, { - teamId: context.user.teamId, - from, - to, - currency: currency ?? undefined, - }), - ]); - - console.log("Database results:", { burnRateData: burnRateData.length, runway, spendingData: spendingData.length }); - - // Early return if no data - if (burnRateData.length === 0) { - await analysis.update({ - stage: "complete", - summary: { - currentBurnRate: 0, - averageRunway: 0, - trend: "stable" as const, - alerts: ["No data available"], - recommendations: ["Check date range selection"], - }, - }); - - yield { text: completeMessage + "\n\nNo burn rate data available for the selected period." }; - return { - currentMonthlyBurn: 0, - runway: 0, - topCategory: "No data", - summary: "No data available", - }; - } - - // Calculate basic metrics from burn rate data - const currentMonthlyBurn = burnRateData[burnRateData.length - 1]?.value || 0; - const averageBurnRate = Math.round( - burnRateData.reduce((sum, item) => sum + item.value, 0) / burnRateData.length - ); - - // Generate monthly chart data - const fromDate = startOfMonth(new Date(from)); - const toDate = endOfMonth(new Date(to)); - const monthSeries = eachMonthOfInterval({ - start: fromDate, - end: toDate, - }); - - const monthlyData = monthSeries.map((month, index) => { - const currentBurn = burnRateData[index]?.value || 0; - return { - month: format(month, "MMM"), - revenue: Math.floor(Math.random() * 30000) + 20000, - expenses: currentBurn + Math.floor(Math.random() * 10000), - burnRate: currentBurn, - runway: Math.max(1, runway - index), - }; - }); - - // Update with chart data - await analysis.update({ - stage: "processing", - chartData: monthlyData, - progress: 0.4, - }); - - yield { text: completeMessage + "\n\nProcessing financial data and generating insights..." }; - - // Get the highest spending category - const highestCategory = spendingData[0] || { - name: "Uncategorized", - slug: "uncategorized", - amount: 0, - percentage: 0, - }; - - // Calculate burn rate change - const burnRateStartValue = burnRateData[0]?.value || 0; - const burnRateEndValue = currentMonthlyBurn; - const burnRateChangePercentage = burnRateStartValue > 0 - ? Math.round(((burnRateEndValue - burnRateStartValue) / burnRateStartValue) * 100) - : 0; - const burnRateChangePeriod = `${burnRateData.length} months`; - - // Update with metrics - await analysis.update({ - stage: "analyzing", - progress: 0.7, - summary: { - currentBurnRate: currentMonthlyBurn, - averageRunway: runway, - trend: burnRateChangePercentage > 5 ? "declining" as const : - burnRateChangePercentage < -5 ? "improving" as const : "stable" as const, - alerts: runway < 6 ? ["Critical: Low runway"] : [], - recommendations: ["Monitor spending trends", "Plan for future growth"], - }, - }); - - const targetCurrency = currency ?? context.user.baseCurrency ?? "USD"; - - // Generate AI summary - const analysisResult = await generateText({ - model: openai("gpt-4o-mini"), - messages: [ - { - role: "user", - content: `Analyze this burn rate data: - -Monthly Burn: ${formatAmount({ amount: currentMonthlyBurn, currency: targetCurrency, locale: context.user.locale })} -Runway: ${runway} months -Change: ${burnRateChangePercentage}% over ${burnRateChangePeriod} -Top Category: ${highestCategory.name} (${highestCategory.percentage}%) - -Provide a concise 2-sentence summary and 2-3 brief recommendations.`, - }, - ], - }); - - const responseText = analysisResult.text; - const lines = responseText.split("\n").filter((line) => line.trim().length > 0); - const summaryText = lines[0] || `Current monthly burn: ${formatAmount({ amount: currentMonthlyBurn, currency: targetCurrency, locale: context.user.locale })} with ${runway}-month runway.`; - const recommendations = lines.slice(1, 4).map((line) => line.replace(/^[-โ€ข*]\s*/, "").trim()).filter((line) => line.length > 0); - - // Final update - await analysis.complete({ - stage: "complete", - title: "Complex Burn Rate Analysis", - chartData: monthlyData, - progress: 1, - summary: { - currentBurnRate: currentMonthlyBurn, - averageRunway: runway, - trend: burnRateChangePercentage > 5 ? "declining" as const : - burnRateChangePercentage < -5 ? "improving" as const : "stable" as const, - alerts: runway < 6 ? ["Critical: Low runway"] : [], - recommendations, - }, - }); - - // Stream the detailed analysis - const burnRateAnalysisData = { - currentMonthlyBurn: formatAmount({ amount: currentMonthlyBurn, currency: targetCurrency, locale: context.user.locale }), - runway, - topCategory: highestCategory.name, - topCategoryPercentage: highestCategory.percentage, - burnRateChange: burnRateChangePercentage, - burnRateChangePeriod, - runwayStatus: runway >= 12 ? "healthy" : runway >= 6 ? "concerning" : "critical", - }; - - const responseStream = streamText({ - model: openai("gpt-4o-mini"), - system: `Generate a detailed burn rate analysis using the provided data. Format it with clear sections for Monthly Burn Rate, Cash Runway, Expense Breakdown, and Trends.`, - messages: [ - { - role: "user", - content: `Generate analysis: ${JSON.stringify(burnRateAnalysisData)}`, - }, - ], - experimental_transform: smoothStream({ chunking: "word" }), - }); - - // Yield the streamed response - let analysisText = ""; - for await (const chunk of responseStream.textStream) { - analysisText += chunk; - yield { text: completeMessage + analysisText }; - } - - completeMessage += analysisText; - - // Generate follow-up questions - const burnRateFollowupQuestions = await generateFollowupQuestions( - "complexBurnRateAnalysis", - completeMessage, - ); - - // Stream follow-up questions artifact - const followupStream = followupQuestionsArtifact.stream({ - questions: burnRateFollowupQuestions, - context: "burn_rate_analysis", - timestamp: new Date().toISOString(), - }); - - await followupStream.complete(); - - // Final yield - yield { - text: completeMessage, - forceStop: true, - }; - - return { - success: true, - currentMonthlyBurn: formatAmount({ amount: currentMonthlyBurn, currency: targetCurrency, locale: context.user.locale }), - runway, - topCategory: highestCategory.name, - summary: summaryText, - analysisComplete: true, - }; - } catch (error) { - console.error("Complex burn rate analysis error:", error); - throw error; - } - }, -}); - -// Create cached version - uses pre-configured cache -export const complexBurnRateAnalysisTool = cachedUpstash(complexBurnRateAnalysis); diff --git a/apps/example/src/ai/agents/tools/customers/create-customer.ts b/apps/example/src/ai/tools/customers/create-customer.ts similarity index 73% rename from apps/example/src/ai/agents/tools/customers/create-customer.ts rename to apps/example/src/ai/tools/customers/create-customer.ts index dbb1f7c..8c0adff 100644 --- a/apps/example/src/ai/agents/tools/customers/create-customer.ts +++ b/apps/example/src/ai/tools/customers/create-customer.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateCreatedCustomer } from "../../utils/fake-data"; +import { generateCreatedCustomer } from "@/ai/utils/fake-data"; export const createCustomerTool = tool({ description: `Create a new customer record.`, @@ -10,9 +10,11 @@ export const createCustomerTool = tool({ email: z.string().email().describe("Customer email"), phone: z.string().optional().describe("Phone number"), address: z.string().optional().describe("Billing address"), - tags: z.array(z.string()).optional().describe("Customer tags for categorization"), + tags: z + .array(z.string()) + .optional() + .describe("Customer tags for categorization"), }), execute: async (params) => generateCreatedCustomer(params), }); - diff --git a/apps/example/src/ai/agents/tools/customers/get-customer.ts b/apps/example/src/ai/tools/customers/get-customer.ts similarity index 86% rename from apps/example/src/ai/agents/tools/customers/get-customer.ts rename to apps/example/src/ai/tools/customers/get-customer.ts index 96f4454..de6b411 100644 --- a/apps/example/src/ai/agents/tools/customers/get-customer.ts +++ b/apps/example/src/ai/tools/customers/get-customer.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateCustomer } from "../../utils/fake-data"; +import { generateCustomer } from "@/ai/utils/fake-data"; export const getCustomerTool = tool({ description: `Get customer details by ID. diff --git a/apps/example/src/ai/agents/tools/customers/index.ts b/apps/example/src/ai/tools/customers/index.ts similarity index 75% rename from apps/example/src/ai/agents/tools/customers/index.ts rename to apps/example/src/ai/tools/customers/index.ts index 89241e8..640ae70 100644 --- a/apps/example/src/ai/agents/tools/customers/index.ts +++ b/apps/example/src/ai/tools/customers/index.ts @@ -1,9 +1,3 @@ -/** - * Customer Tools - * - * Tools for customer management and analysis - */ - export { createCustomerTool } from "./create-customer"; export { getCustomerTool } from "./get-customer"; export { customerProfitabilityTool } from "./profitability-analysis"; diff --git a/apps/example/src/ai/agents/tools/customers/profitability-analysis.ts b/apps/example/src/ai/tools/customers/profitability-analysis.ts similarity index 78% rename from apps/example/src/ai/agents/tools/customers/profitability-analysis.ts rename to apps/example/src/ai/tools/customers/profitability-analysis.ts index 481efdd..720a153 100644 --- a/apps/example/src/ai/agents/tools/customers/profitability-analysis.ts +++ b/apps/example/src/ai/tools/customers/profitability-analysis.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { dateRangeSchema } from "../../types/filters"; -import { generateCustomerProfitability } from "../../utils/fake-data"; +import { dateRangeSchema } from "@/ai/types/filters"; +import { generateCustomerProfitability } from "@/ai/utils/fake-data"; export const customerProfitabilityTool = tool({ description: `Analyze customer profitability using revenue, costs, and tags. diff --git a/apps/example/src/ai/agents/tools/customers/update-customer.ts b/apps/example/src/ai/tools/customers/update-customer.ts similarity index 90% rename from apps/example/src/ai/agents/tools/customers/update-customer.ts rename to apps/example/src/ai/tools/customers/update-customer.ts index 908c4d4..29d31fc 100644 --- a/apps/example/src/ai/agents/tools/customers/update-customer.ts +++ b/apps/example/src/ai/tools/customers/update-customer.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateUpdatedCustomer } from "../../utils/fake-data"; +import { generateUpdatedCustomer } from "@/ai/utils/fake-data"; export const updateCustomerTool = tool({ description: `Update customer information.`, diff --git a/apps/example/src/ai/tools/financial-tools.ts b/apps/example/src/ai/tools/financial-tools.ts deleted file mode 100644 index 605121d..0000000 --- a/apps/example/src/ai/tools/financial-tools.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { tool } from "ai"; -import { z } from "zod"; - -// Mock transaction data -const mockTransactions = [ - { - id: "tx001", - date: "2024-01-15", - amount: 1250.0, - category: "Software", - vendor: "Adobe Inc", - type: "expense", - }, - { - id: "tx002", - date: "2024-01-16", - amount: 3500.0, - category: "Marketing", - vendor: "Google Ads", - type: "expense", - }, - { - id: "tx003", - date: "2024-01-17", - amount: 15000.0, - category: "Revenue", - vendor: "Client ABC", - type: "income", - }, - { - id: "tx004", - date: "2024-01-18", - amount: 800.0, - category: "Office", - vendor: "WeWork", - type: "expense", - }, - { - id: "tx005", - date: "2024-01-19", - amount: 2200.0, - category: "Payroll", - vendor: "ADP", - type: "expense", - }, - { - id: "tx006", - date: "2024-01-20", - amount: 12000.0, - category: "Revenue", - vendor: "Client XYZ", - type: "income", - }, - { - id: "tx007", - date: "2024-01-21", - amount: 450.0, - category: "Travel", - vendor: "Delta Airlines", - type: "expense", - }, - { - id: "tx008", - date: "2024-01-22", - amount: 1800.0, - category: "Equipment", - vendor: "Apple Store", - type: "expense", - }, -]; - -export const queryTransactionsTool = tool({ - description: "Query and analyze transaction data", - inputSchema: z.object({ - filters: z.object({ - category: z.string().optional().describe("Filter by category"), - type: z - .enum(["income", "expense", "all"]) - .optional() - .describe("Filter by transaction type"), - dateFrom: z.string().optional().describe("Start date (YYYY-MM-DD)"), - dateTo: z.string().optional().describe("End date (YYYY-MM-DD)"), - minAmount: z.number().optional().describe("Minimum amount"), - maxAmount: z.number().optional().describe("Maximum amount"), - }), - analysis: z - .enum(["summary", "totals", "trends", "categories"]) - .describe("Type of analysis to perform"), - }), - execute: async ({ filters, analysis }) => { - let filteredTransactions = mockTransactions; - - // Apply filters - if (filters.category) { - filteredTransactions = filteredTransactions.filter((t) => - t.category.toLowerCase().includes(filters.category?.toLowerCase()), - ); - } - if (filters.type && filters.type !== "all") { - filteredTransactions = filteredTransactions.filter( - (t) => t.type === filters.type, - ); - } - if (filters.minAmount) { - filteredTransactions = filteredTransactions.filter( - (t) => t.amount >= filters.minAmount!, - ); - } - if (filters.maxAmount) { - filteredTransactions = filteredTransactions.filter( - (t) => t.amount <= filters.maxAmount!, - ); - } - - // Perform analysis - switch (analysis) { - case "summary": - return { - totalTransactions: filteredTransactions.length, - totalIncome: filteredTransactions - .filter((t) => t.type === "income") - .reduce((sum, t) => sum + t.amount, 0), - totalExpenses: filteredTransactions - .filter((t) => t.type === "expense") - .reduce((sum, t) => sum + t.amount, 0), - transactions: filteredTransactions, - }; - - case "totals": { - const income = filteredTransactions - .filter((t) => t.type === "income") - .reduce((sum, t) => sum + t.amount, 0); - const expenses = filteredTransactions - .filter((t) => t.type === "expense") - .reduce((sum, t) => sum + t.amount, 0); - return { - totalIncome: income, - totalExpenses: expenses, - netIncome: income - expenses, - count: filteredTransactions.length, - }; - } - - case "categories": { - const categoryTotals = filteredTransactions.reduce( - (acc, t) => { - acc[t.category] = (acc[t.category] || 0) + t.amount; - return acc; - }, - {} as Record, - ); - return { categoryBreakdown: categoryTotals }; - } - - case "trends": - return { - message: - "Trend analysis shows consistent revenue growth with controlled expense management", - transactions: filteredTransactions.slice(0, 5), // Recent transactions - }; - - default: - return { transactions: filteredTransactions }; - } - }, -}); - -export const generateChartTool = tool({ - description: "Generate chart data for visualizations", - inputSchema: z.object({ - chartType: z - .enum(["bar", "line", "pie", "area"]) - .describe("Type of chart to generate"), - dataType: z - .enum(["revenue", "expenses", "categories", "trends"]) - .describe("What data to chart"), - period: z - .enum(["daily", "weekly", "monthly"]) - .optional() - .describe("Time period for the chart"), - }), - execute: async ({ chartType, dataType, period = "daily" }) => { - // Generate mock chart data based on our transactions - switch (dataType) { - case "revenue": - return { - chartType, - data: [ - { label: "Jan Week 1", value: 15000 }, - { label: "Jan Week 2", value: 12000 }, - { label: "Jan Week 3", value: 18000 }, - { label: "Jan Week 4", value: 22000 }, - ], - title: "Revenue Trends", - }; - - case "expenses": - return { - chartType, - data: [ - { label: "Software", value: 1250 }, - { label: "Marketing", value: 3500 }, - { label: "Office", value: 800 }, - { label: "Payroll", value: 2200 }, - { label: "Travel", value: 450 }, - { label: "Equipment", value: 1800 }, - ], - title: "Expense Breakdown", - }; - - case "categories": { - const categoryData = mockTransactions.reduce( - (acc, t) => { - acc[t.category] = (acc[t.category] || 0) + t.amount; - return acc; - }, - {} as Record, - ); - - return { - chartType, - data: Object.entries(categoryData).map(([label, value]) => ({ - label, - value, - })), - title: "Spending by Category", - }; - } - - default: - return { - chartType, - data: [], - title: "Chart Data", - }; - } - }, -}); diff --git a/apps/example/src/ai/tools/index.ts b/apps/example/src/ai/tools/index.ts deleted file mode 100644 index ffe0b51..0000000 --- a/apps/example/src/ai/tools/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { analyzeBurnRateTool, cachedAnalyzeBurnRateTool } from "./burn-rate"; -import { complexBurnRateAnalysis, complexBurnRateAnalysisTool } from "./complex-burn-rate"; - -// Export production tools -export const tools = { - analyzeBurnRate: cachedAnalyzeBurnRateTool, // Simple cached version - complexAnalysis: complexBurnRateAnalysisTool, // Complex cached version (mirrors real app) - complexAnalysisUncached: complexBurnRateAnalysis, // Uncached for comparison -}; - -// Also export original for comparison -export const originalTools = { - analyzeBurnRate: analyzeBurnRateTool, -}; diff --git a/apps/example/src/ai/agents/tools/invoices/create-invoice.ts b/apps/example/src/ai/tools/invoices/create-invoice.ts similarity index 95% rename from apps/example/src/ai/agents/tools/invoices/create-invoice.ts rename to apps/example/src/ai/tools/invoices/create-invoice.ts index bede15a..5d89068 100644 --- a/apps/example/src/ai/agents/tools/invoices/create-invoice.ts +++ b/apps/example/src/ai/tools/invoices/create-invoice.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateCreatedInvoice } from "../../utils/fake-data"; +import { generateCreatedInvoice } from "@/ai/utils/fake-data"; /** * Create Invoice Tool diff --git a/apps/example/src/ai/agents/tools/invoices/get-invoice.ts b/apps/example/src/ai/tools/invoices/get-invoice.ts similarity index 91% rename from apps/example/src/ai/agents/tools/invoices/get-invoice.ts rename to apps/example/src/ai/tools/invoices/get-invoice.ts index 76bd453..241d661 100644 --- a/apps/example/src/ai/agents/tools/invoices/get-invoice.ts +++ b/apps/example/src/ai/tools/invoices/get-invoice.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateInvoice } from "../../utils/fake-data"; +import { generateInvoice } from "@/ai/utils/fake-data"; /** * Get Invoice Tool diff --git a/apps/example/src/ai/agents/tools/invoices/index.ts b/apps/example/src/ai/tools/invoices/index.ts similarity index 74% rename from apps/example/src/ai/agents/tools/invoices/index.ts rename to apps/example/src/ai/tools/invoices/index.ts index 240bd45..2edd962 100644 --- a/apps/example/src/ai/agents/tools/invoices/index.ts +++ b/apps/example/src/ai/tools/invoices/index.ts @@ -1,9 +1,3 @@ -/** - * Invoice Tools - * - * Tools for querying and managing invoices - */ - export { createInvoiceTool } from "./create-invoice"; export { getInvoiceTool } from "./get-invoice"; export { listInvoicesTool } from "./list-invoices"; diff --git a/apps/example/src/ai/agents/tools/invoices/list-invoices.ts b/apps/example/src/ai/tools/invoices/list-invoices.ts similarity index 95% rename from apps/example/src/ai/agents/tools/invoices/list-invoices.ts rename to apps/example/src/ai/tools/invoices/list-invoices.ts index 3b45f0a..984cf39 100644 --- a/apps/example/src/ai/agents/tools/invoices/list-invoices.ts +++ b/apps/example/src/ai/tools/invoices/list-invoices.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateInvoices } from "../../utils/fake-data"; +import { generateInvoices } from "@/ai/utils/fake-data"; /** * List Invoices Tool diff --git a/apps/example/src/ai/agents/tools/invoices/update-invoice.ts b/apps/example/src/ai/tools/invoices/update-invoice.ts similarity index 95% rename from apps/example/src/ai/agents/tools/invoices/update-invoice.ts rename to apps/example/src/ai/tools/invoices/update-invoice.ts index 8f641da..1922a07 100644 --- a/apps/example/src/ai/agents/tools/invoices/update-invoice.ts +++ b/apps/example/src/ai/tools/invoices/update-invoice.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateUpdatedInvoice } from "../../utils/fake-data"; +import { generateUpdatedInvoice } from "@/ai/utils/fake-data"; /** * Update Invoice Tool diff --git a/apps/example/src/ai/agents/tools/operations/export-data.ts b/apps/example/src/ai/tools/operations/export-data.ts similarity index 83% rename from apps/example/src/ai/agents/tools/operations/export-data.ts rename to apps/example/src/ai/tools/operations/export-data.ts index 34ab13e..c048b25 100644 --- a/apps/example/src/ai/agents/tools/operations/export-data.ts +++ b/apps/example/src/ai/tools/operations/export-data.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { dateRangeSchema } from "../../types/filters"; -import { generateDataExport } from "../../utils/fake-data"; +import { dateRangeSchema } from "@/ai/types/filters"; +import { generateDataExport } from "@/ai/utils/fake-data"; export const exportDataTool = tool({ description: `Export financial data in various formats. diff --git a/apps/example/src/ai/agents/tools/operations/get-balances.ts b/apps/example/src/ai/tools/operations/get-balances.ts similarity index 90% rename from apps/example/src/ai/agents/tools/operations/get-balances.ts rename to apps/example/src/ai/tools/operations/get-balances.ts index a83906d..9e7555e 100644 --- a/apps/example/src/ai/agents/tools/operations/get-balances.ts +++ b/apps/example/src/ai/tools/operations/get-balances.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateBalances } from "../../utils/fake-data"; +import { generateBalances } from "@/ai/utils/fake-data"; export const getBalancesTool = tool({ description: `Get account balances across all accounts or by specific account. diff --git a/apps/example/src/ai/agents/tools/operations/index.ts b/apps/example/src/ai/tools/operations/index.ts similarity index 69% rename from apps/example/src/ai/agents/tools/operations/index.ts rename to apps/example/src/ai/tools/operations/index.ts index be99f41..253b2af 100644 --- a/apps/example/src/ai/agents/tools/operations/index.ts +++ b/apps/example/src/ai/tools/operations/index.ts @@ -1,9 +1,3 @@ -/** - * Operations Tools - * - * Tools for inbox, balances, documents, and data export - */ - export { exportDataTool } from "./export-data"; export { getBalancesTool } from "./get-balances"; export { listDocumentsTool } from "./list-documents"; diff --git a/apps/example/src/ai/agents/tools/operations/list-documents.ts b/apps/example/src/ai/tools/operations/list-documents.ts similarity index 89% rename from apps/example/src/ai/agents/tools/operations/list-documents.ts rename to apps/example/src/ai/tools/operations/list-documents.ts index 36257ae..4b54fae 100644 --- a/apps/example/src/ai/agents/tools/operations/list-documents.ts +++ b/apps/example/src/ai/tools/operations/list-documents.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateDocuments } from "../../utils/fake-data"; +import { generateDocuments } from "@/ai/utils/fake-data"; export const listDocumentsTool = tool({ description: `List stored documents with filtering and search. diff --git a/apps/example/src/ai/agents/tools/operations/list-inbox.ts b/apps/example/src/ai/tools/operations/list-inbox.ts similarity index 89% rename from apps/example/src/ai/agents/tools/operations/list-inbox.ts rename to apps/example/src/ai/tools/operations/list-inbox.ts index 206aaac..a7e9add 100644 --- a/apps/example/src/ai/agents/tools/operations/list-inbox.ts +++ b/apps/example/src/ai/tools/operations/list-inbox.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateInboxItems } from "../../utils/fake-data"; +import { generateInboxItems } from "@/ai/utils/fake-data"; export const listInboxItemsTool = tool({ description: `List items in the inbox (receipts, documents awaiting processing). diff --git a/apps/example/src/ai/agents/tools/reports/balance-sheet.ts b/apps/example/src/ai/tools/reports/balance-sheet.ts similarity index 74% rename from apps/example/src/ai/agents/tools/reports/balance-sheet.ts rename to apps/example/src/ai/tools/reports/balance-sheet.ts index 20c4be7..fbe553f 100644 --- a/apps/example/src/ai/agents/tools/reports/balance-sheet.ts +++ b/apps/example/src/ai/tools/reports/balance-sheet.ts @@ -1,9 +1,10 @@ +import { getWriter } from "@ai-sdk-tools/artifacts"; import { tool } from "ai"; import { z } from "zod"; import { BalanceSheetArtifact } from "@/ai/artifacts/balance-sheet"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateBalanceSheet } from "@/ai/utils/fake-data"; import { delay } from "@/lib/delay"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateBalanceSheet } from "../../utils/fake-data"; /** * Balance Sheet Tool @@ -37,63 +38,71 @@ Capabilities: .describe("Use interactive artifact visualization"), }), - execute: async function* ({ from, to, currency, categories, useArtifact }) { + execute: async function* ( + { from, to, currency, categories, useArtifact }, + executionOptions, + ) { + const writer = getWriter(executionOptions); + if (!useArtifact) { // Legacy mode - return raw data return generateBalanceSheet({ from, to, currency, categories }); } // Artifact mode - stream the balance sheet with visualization - const analysis = BalanceSheetArtifact.stream({ - stage: "loading", - title: `Balance Sheet as of ${to}`, - asOfDate: to, - currency: currency || "USD", - progress: 0, - assets: { - currentAssets: { - cash: 0, - accountsReceivable: 0, - inventory: 0, - prepaidExpenses: 0, - total: 0, + const analysis = BalanceSheetArtifact.stream( + { + stage: "loading", + title: `Balance Sheet as of ${to}`, + asOfDate: to, + currency: currency || "USD", + progress: 0, + assets: { + currentAssets: { + cash: 0, + accountsReceivable: 0, + inventory: 0, + prepaidExpenses: 0, + total: 0, + }, + nonCurrentAssets: { + propertyPlantEquipment: 0, + intangibleAssets: 0, + investments: 0, + total: 0, + }, + totalAssets: 0, }, - nonCurrentAssets: { - propertyPlantEquipment: 0, - intangibleAssets: 0, - investments: 0, - total: 0, + liabilities: { + currentLiabilities: { + accountsPayable: 0, + shortTermDebt: 0, + accruedExpenses: 0, + total: 0, + }, + nonCurrentLiabilities: { + longTermDebt: 0, + deferredRevenue: 0, + otherLiabilities: 0, + total: 0, + }, + totalLiabilities: 0, }, - totalAssets: 0, - }, - liabilities: { - currentLiabilities: { - accountsPayable: 0, - shortTermDebt: 0, - accruedExpenses: 0, - total: 0, + equity: { + commonStock: 0, + retainedEarnings: 0, + additionalPaidInCapital: 0, + totalEquity: 0, }, - nonCurrentLiabilities: { - longTermDebt: 0, - deferredRevenue: 0, - otherLiabilities: 0, - total: 0, + ratios: { + currentRatio: 0, + quickRatio: 0, + debtToEquity: 0, + workingCapital: 0, }, - totalLiabilities: 0, }, - equity: { - commonStock: 0, - retainedEarnings: 0, - additionalPaidInCapital: 0, - totalEquity: 0, - }, - ratios: { - currentRatio: 0, - quickRatio: 0, - debtToEquity: 0, - workingCapital: 0, - }, - }); + writer, + ); yield { text: `Generating balance sheet for ${to}...` }; await delay(300); diff --git a/apps/example/src/ai/agents/tools/reports/burn-rate.ts b/apps/example/src/ai/tools/reports/burn-rate.ts similarity index 80% rename from apps/example/src/ai/agents/tools/reports/burn-rate.ts rename to apps/example/src/ai/tools/reports/burn-rate.ts index 24c8421..1e7c9c5 100644 --- a/apps/example/src/ai/agents/tools/reports/burn-rate.ts +++ b/apps/example/src/ai/tools/reports/burn-rate.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateBurnRateMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateBurnRateMetrics } from "@/ai/utils/fake-data"; /** * Burn Rate Metrics Tool diff --git a/apps/example/src/ai/agents/tools/reports/cash-flow.ts b/apps/example/src/ai/tools/reports/cash-flow.ts similarity index 85% rename from apps/example/src/ai/agents/tools/reports/cash-flow.ts rename to apps/example/src/ai/tools/reports/cash-flow.ts index 73f4121..ea428a5 100644 --- a/apps/example/src/ai/agents/tools/reports/cash-flow.ts +++ b/apps/example/src/ai/tools/reports/cash-flow.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateCashFlowMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateCashFlowMetrics } from "@/ai/utils/fake-data"; /** * Cash Flow Analysis Tool diff --git a/apps/example/src/ai/agents/tools/reports/expenses.ts b/apps/example/src/ai/tools/reports/expenses.ts similarity index 86% rename from apps/example/src/ai/agents/tools/reports/expenses.ts rename to apps/example/src/ai/tools/reports/expenses.ts index 0a51549..ee53a72 100644 --- a/apps/example/src/ai/agents/tools/reports/expenses.ts +++ b/apps/example/src/ai/tools/reports/expenses.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateExpensesMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateExpensesMetrics } from "@/ai/utils/fake-data"; /** * Expenses Analysis Tool diff --git a/apps/example/src/ai/agents/tools/reports/index.ts b/apps/example/src/ai/tools/reports/index.ts similarity index 80% rename from apps/example/src/ai/agents/tools/reports/index.ts rename to apps/example/src/ai/tools/reports/index.ts index 33e3907..db572ee 100644 --- a/apps/example/src/ai/agents/tools/reports/index.ts +++ b/apps/example/src/ai/tools/reports/index.ts @@ -1,9 +1,3 @@ -/** - * Financial Reports Tools - * - * Collection of tools for generating various financial reports - */ - export { balanceSheetTool } from "./balance-sheet"; export { burnRateMetricsTool } from "./burn-rate"; export { cashFlowTool } from "./cash-flow"; diff --git a/apps/example/src/ai/agents/tools/reports/profit-loss.ts b/apps/example/src/ai/tools/reports/profit-loss.ts similarity index 80% rename from apps/example/src/ai/agents/tools/reports/profit-loss.ts rename to apps/example/src/ai/tools/reports/profit-loss.ts index 0a7cdd6..26272a8 100644 --- a/apps/example/src/ai/agents/tools/reports/profit-loss.ts +++ b/apps/example/src/ai/tools/reports/profit-loss.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateProfitLossMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateProfitLossMetrics } from "@/ai/utils/fake-data"; /** * Profit & Loss (P&L) Tool diff --git a/apps/example/src/ai/agents/tools/reports/revenue.ts b/apps/example/src/ai/tools/reports/revenue.ts similarity index 97% rename from apps/example/src/ai/agents/tools/reports/revenue.ts rename to apps/example/src/ai/tools/reports/revenue.ts index b17eac7..1afbf4c 100644 --- a/apps/example/src/ai/agents/tools/reports/revenue.ts +++ b/apps/example/src/ai/tools/reports/revenue.ts @@ -1,9 +1,9 @@ import { tool } from "ai"; import { z } from "zod"; import { RevenueArtifact } from "@/ai/artifacts/revenue"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateRevenueMetrics } from "@/ai/utils/fake-data"; import { delay } from "@/lib/delay"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateRevenueMetrics } from "../../utils/fake-data"; /** * Revenue Dashboard Tool diff --git a/apps/example/src/ai/agents/tools/reports/runway.ts b/apps/example/src/ai/tools/reports/runway.ts similarity index 81% rename from apps/example/src/ai/agents/tools/reports/runway.ts rename to apps/example/src/ai/tools/reports/runway.ts index fdec0f1..c88ba70 100644 --- a/apps/example/src/ai/agents/tools/reports/runway.ts +++ b/apps/example/src/ai/tools/reports/runway.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateRunwayMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateRunwayMetrics } from "@/ai/utils/fake-data"; /** * Runway Metrics Tool diff --git a/apps/example/src/ai/agents/tools/reports/spending.ts b/apps/example/src/ai/tools/reports/spending.ts similarity index 87% rename from apps/example/src/ai/agents/tools/reports/spending.ts rename to apps/example/src/ai/tools/reports/spending.ts index 599b2ae..dcddad0 100644 --- a/apps/example/src/ai/agents/tools/reports/spending.ts +++ b/apps/example/src/ai/tools/reports/spending.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateSpendingMetrics } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateSpendingMetrics } from "@/ai/utils/fake-data"; /** * Spending Analysis Tool diff --git a/apps/example/src/ai/agents/tools/reports/tax-summary.ts b/apps/example/src/ai/tools/reports/tax-summary.ts similarity index 86% rename from apps/example/src/ai/agents/tools/reports/tax-summary.ts rename to apps/example/src/ai/tools/reports/tax-summary.ts index 5170808..b6f8982 100644 --- a/apps/example/src/ai/agents/tools/reports/tax-summary.ts +++ b/apps/example/src/ai/tools/reports/tax-summary.ts @@ -1,7 +1,7 @@ import { tool } from "ai"; import { z } from "zod"; -import { currencyFilterSchema, dateRangeSchema } from "../../types/filters"; -import { generateTaxSummary } from "../../utils/fake-data"; +import { currencyFilterSchema, dateRangeSchema } from "@/ai/types/filters"; +import { generateTaxSummary } from "@/ai/utils/fake-data"; /** * Tax Summary Tool diff --git a/apps/example/src/ai/tools/schema.ts b/apps/example/src/ai/tools/schema.ts deleted file mode 100644 index 5ca96e6..0000000 --- a/apps/example/src/ai/tools/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from "zod"; - -export const getBurnRateSchema = z.object({ - from: z.string().describe("Start date in YYYY-MM-DD format"), - to: z.string().describe("End date in YYYY-MM-DD format"), - currency: z.string().optional().describe("Currency code (USD, EUR, SEK, etc.)"), - showCanvas: z.boolean().optional().describe("Whether to show the analysis canvas"), -}); diff --git a/apps/example/src/ai/agents/tools/tracker/create-time-entry.ts b/apps/example/src/ai/tools/tracker/create-time-entry.ts similarity index 91% rename from apps/example/src/ai/agents/tools/tracker/create-time-entry.ts rename to apps/example/src/ai/tools/tracker/create-time-entry.ts index 050b3fe..62ec981 100644 --- a/apps/example/src/ai/agents/tools/tracker/create-time-entry.ts +++ b/apps/example/src/ai/tools/tracker/create-time-entry.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateCreatedTimeEntry } from "../../utils/fake-data"; +import { generateCreatedTimeEntry } from "@/ai/utils/fake-data"; export const createTimeEntryTool = tool({ description: `Create a manual time entry (not from timer). diff --git a/apps/example/src/ai/agents/tools/tracker/delete-time-entry.ts b/apps/example/src/ai/tools/tracker/delete-time-entry.ts similarity index 82% rename from apps/example/src/ai/agents/tools/tracker/delete-time-entry.ts rename to apps/example/src/ai/tools/tracker/delete-time-entry.ts index 6dd70e5..e08fc7f 100644 --- a/apps/example/src/ai/agents/tools/tracker/delete-time-entry.ts +++ b/apps/example/src/ai/tools/tracker/delete-time-entry.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateDeletedTimeEntry } from "../../utils/fake-data"; +import { generateDeletedTimeEntry } from "@/ai/utils/fake-data"; export const deleteTimeEntryTool = tool({ description: `Delete a time tracking entry.`, diff --git a/apps/example/src/ai/agents/tools/tracker/get-time-entries.ts b/apps/example/src/ai/tools/tracker/get-time-entries.ts similarity index 90% rename from apps/example/src/ai/agents/tools/tracker/get-time-entries.ts rename to apps/example/src/ai/tools/tracker/get-time-entries.ts index 482f181..5bb1365 100644 --- a/apps/example/src/ai/agents/tools/tracker/get-time-entries.ts +++ b/apps/example/src/ai/tools/tracker/get-time-entries.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateTimeEntries } from "../../utils/fake-data"; +import { generateTimeEntries } from "@/ai/utils/fake-data"; export const getTimeEntriesTool = tool({ description: `Get time tracking entries with filtering options. diff --git a/apps/example/src/ai/agents/tools/tracker/get-tracker-projects.ts b/apps/example/src/ai/tools/tracker/get-tracker-projects.ts similarity index 87% rename from apps/example/src/ai/agents/tools/tracker/get-tracker-projects.ts rename to apps/example/src/ai/tools/tracker/get-tracker-projects.ts index 5f8df28..15f5d78 100644 --- a/apps/example/src/ai/agents/tools/tracker/get-tracker-projects.ts +++ b/apps/example/src/ai/tools/tracker/get-tracker-projects.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateTrackerProjects } from "../../utils/fake-data"; +import { generateTrackerProjects } from "@/ai/utils/fake-data"; export const getTrackerProjectsTool = tool({ description: `Get list of time tracking projects. diff --git a/apps/example/src/ai/agents/tools/tracker/index.ts b/apps/example/src/ai/tools/tracker/index.ts similarity index 81% rename from apps/example/src/ai/agents/tools/tracker/index.ts rename to apps/example/src/ai/tools/tracker/index.ts index 059581b..76489ad 100644 --- a/apps/example/src/ai/agents/tools/tracker/index.ts +++ b/apps/example/src/ai/tools/tracker/index.ts @@ -1,9 +1,3 @@ -/** - * Time Tracker Tools - * - * Tools for time tracking, projects, and time entries - */ - export { createTimeEntryTool } from "./create-time-entry"; export { deleteTimeEntryTool } from "./delete-time-entry"; export { getTimeEntriesTool } from "./get-time-entries"; diff --git a/apps/example/src/ai/agents/tools/tracker/start-timer.ts b/apps/example/src/ai/tools/tracker/start-timer.ts similarity index 92% rename from apps/example/src/ai/agents/tools/tracker/start-timer.ts rename to apps/example/src/ai/tools/tracker/start-timer.ts index edb7aa1..f619f69 100644 --- a/apps/example/src/ai/agents/tools/tracker/start-timer.ts +++ b/apps/example/src/ai/tools/tracker/start-timer.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateStartedTimer } from "../../utils/fake-data"; +import { generateStartedTimer } from "@/ai/utils/fake-data"; /** * Start Timer Tool diff --git a/apps/example/src/ai/agents/tools/tracker/stop-timer.ts b/apps/example/src/ai/tools/tracker/stop-timer.ts similarity index 89% rename from apps/example/src/ai/agents/tools/tracker/stop-timer.ts rename to apps/example/src/ai/tools/tracker/stop-timer.ts index 4b3758e..9307ebe 100644 --- a/apps/example/src/ai/agents/tools/tracker/stop-timer.ts +++ b/apps/example/src/ai/tools/tracker/stop-timer.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateStoppedTimer } from "../../utils/fake-data"; +import { generateStoppedTimer } from "@/ai/utils/fake-data"; export const stopTimerTool = tool({ description: `Stop the currently running timer or a specific timer entry.`, diff --git a/apps/example/src/ai/agents/tools/tracker/update-time-entry.ts b/apps/example/src/ai/tools/tracker/update-time-entry.ts similarity index 89% rename from apps/example/src/ai/agents/tools/tracker/update-time-entry.ts rename to apps/example/src/ai/tools/tracker/update-time-entry.ts index 3ff486f..68d4994 100644 --- a/apps/example/src/ai/agents/tools/tracker/update-time-entry.ts +++ b/apps/example/src/ai/tools/tracker/update-time-entry.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateUpdatedTimeEntry } from "../../utils/fake-data"; +import { generateUpdatedTimeEntry } from "@/ai/utils/fake-data"; export const updateTimeEntryTool = tool({ description: `Update an existing time entry.`, diff --git a/apps/example/src/ai/agents/tools/transactions/get-transaction.ts b/apps/example/src/ai/tools/transactions/get-transaction.ts similarity index 90% rename from apps/example/src/ai/agents/tools/transactions/get-transaction.ts rename to apps/example/src/ai/tools/transactions/get-transaction.ts index 94c3a4b..a77260e 100644 --- a/apps/example/src/ai/agents/tools/transactions/get-transaction.ts +++ b/apps/example/src/ai/tools/transactions/get-transaction.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateTransaction } from "../../utils/fake-data"; +import { generateTransaction } from "@/ai/utils/fake-data"; /** * Get Transaction Tool diff --git a/apps/example/src/ai/agents/tools/transactions/index.ts b/apps/example/src/ai/tools/transactions/index.ts similarity index 58% rename from apps/example/src/ai/agents/tools/transactions/index.ts rename to apps/example/src/ai/tools/transactions/index.ts index 1b74816..84a8676 100644 --- a/apps/example/src/ai/agents/tools/transactions/index.ts +++ b/apps/example/src/ai/tools/transactions/index.ts @@ -1,8 +1,2 @@ -/** - * Transaction Tools - * - * Tools for querying and managing transactions - */ - export { getTransactionTool } from "./get-transaction"; export { listTransactionsTool } from "./list-transactions"; diff --git a/apps/example/src/ai/agents/tools/transactions/list-transactions.ts b/apps/example/src/ai/tools/transactions/list-transactions.ts similarity index 96% rename from apps/example/src/ai/agents/tools/transactions/list-transactions.ts rename to apps/example/src/ai/tools/transactions/list-transactions.ts index 28c1f62..a62265c 100644 --- a/apps/example/src/ai/agents/tools/transactions/list-transactions.ts +++ b/apps/example/src/ai/tools/transactions/list-transactions.ts @@ -1,6 +1,6 @@ import { tool } from "ai"; import { z } from "zod"; -import { generateTransactions } from "../../utils/fake-data"; +import { generateTransactions } from "@/ai/utils/fake-data"; /** * List Transactions Tool diff --git a/apps/example/src/ai/agents/types/filters.ts b/apps/example/src/ai/types/filters.ts similarity index 100% rename from apps/example/src/ai/agents/types/filters.ts rename to apps/example/src/ai/types/filters.ts diff --git a/apps/example/src/ai/agents/utils/date-helpers.ts b/apps/example/src/ai/utils/date-helpers.ts similarity index 100% rename from apps/example/src/ai/agents/utils/date-helpers.ts rename to apps/example/src/ai/utils/date-helpers.ts diff --git a/apps/example/src/ai/agents/utils/fake-data.ts b/apps/example/src/ai/utils/fake-data.ts similarity index 100% rename from apps/example/src/ai/agents/utils/fake-data.ts rename to apps/example/src/ai/utils/fake-data.ts diff --git a/apps/example/src/ai/agents/utils/filter-builders.ts b/apps/example/src/ai/utils/filter-builders.ts similarity index 100% rename from apps/example/src/ai/agents/utils/filter-builders.ts rename to apps/example/src/ai/utils/filter-builders.ts diff --git a/apps/example/src/app/api/chat/route.ts b/apps/example/src/app/api/chat/route.ts index 36b9bce..293efbf 100644 --- a/apps/example/src/app/api/chat/route.ts +++ b/apps/example/src/app/api/chat/route.ts @@ -1,711 +1,65 @@ -// NOTE: This is what will become @ai-sdk-tools/agents, make it work, make it right, make it good. -// https://ai-sdk-tools.dev/agents - -import { openai } from "@ai-sdk/openai"; -import { - Experimental_Agent as Agent, - convertToModelMessages, - createUIMessageStream, - createUIMessageStreamResponse, - smoothStream, - tool, -} from "ai"; -import { z } from "zod"; -import { - businessHealthScoreTool, - cashFlowForecastTool, - cashFlowStressTestTool, -} from "@/ai/agents/tools/analytics"; -import { - createCustomerTool, - customerProfitabilityTool, - getCustomerTool, - updateCustomerTool, -} from "@/ai/agents/tools/customers"; -import { - createInvoiceTool, - getInvoiceTool, - listInvoicesTool, - updateInvoiceTool, -} from "@/ai/agents/tools/invoices"; -import { - exportDataTool, - getBalancesTool, - listDocumentsTool, - listInboxItemsTool, -} from "@/ai/agents/tools/operations"; -import { - balanceSheetTool, - burnRateMetricsTool, - cashFlowTool, - expensesTool, - profitLossTool, - revenueDashboardTool, - runwayMetricsTool, - spendingMetricsTool, - taxSummaryTool, -} from "@/ai/agents/tools/reports"; -import { - createTimeEntryTool, - deleteTimeEntryTool, - getTimeEntriesTool, - getTrackerProjectsTool, - startTimerTool, - stopTimerTool, - updateTimeEntryTool, -} from "@/ai/agents/tools/tracker"; -import { - getTransactionTool, - listTransactionsTool, -} from "@/ai/agents/tools/transactions"; -import { setContext } from "@/ai/context"; +import type { AgentEvent } from "@ai-sdk-tools/agents"; +import { convertToModelMessages, smoothStream } from "ai"; +import type { NextRequest } from "next/server"; +import { buildAppContext } from "@/ai/agents/shared"; +import { triageAgent } from "@/ai/agents/triage"; import { checkRateLimit, getClientIP } from "@/lib/rate-limiter"; -import type { AgentUIMessage } from "@/types/agents"; -import { classifyIntent, RECOMMENDED_PROMPT_PREFIX } from "./routing"; - -/** - * Multi-Agent Financial Assistant - * - * Architecture: - * - HYBRID ROUTING: Programmatic classifier for 90% of cases, LLM fallback for complex queries - * - Specialists execute tools and present results - * - Sends real-time status updates via transient data parts - */ - -/** - * Extract text from a UI message (handles various AI SDK message formats) - */ -function extractMessageText(message: unknown): string { - if (!message || typeof message !== "object") return ""; - const msg = message as { content?: unknown; parts?: unknown[] }; - - // String content - if (typeof msg.content === "string") return msg.content; - - // Find text in parts array - const findTextPart = (items?: unknown[]) => - items?.find( - (item): item is { text: string } => - typeof item === "object" && - item !== null && - "type" in item && - item.type === "text", - )?.text || ""; - - return findTextPart(msg.parts) || findTextPart(msg.content as unknown[]); -} - -// Configuration -const CONFIG = { - orchestration: { - maxRounds: 5, - contextWindow: 5, // Number of recent messages for specialists - }, - agents: { - maxSteps: 5, - }, -} as const; - -/** - * Agent Context - * TODO: Later pass as parameter to orchestrator.run({ context }) - */ -const AGENT_CONTEXT = { - companyName: "Acme Inc.", - date: new Date().toISOString().split("T")[0], // YYYY-MM-DD - fullName: "John Doe", - registeredCountry: "United States", -} as const; - -/** - * Format context for system prompts - */ -function getContextPrompt(): string { - return ` -CONTEXT: -- Company: ${AGENT_CONTEXT.companyName} -- Date: ${AGENT_CONTEXT.date} -- User: ${AGENT_CONTEXT.fullName} -- Country: ${AGENT_CONTEXT.registeredCountry} -`.trim(); -} - -// Specialist Agents -const specialists = { - reports: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a financial reports specialist with access to live financial data. - -YOUR SCOPE: Provide specific financial reports (revenue, P&L, cash flow, etc.) -NOT YOUR SCOPE: Business health analysis, forecasting (those go to analytics specialist) - -CRITICAL RULES: -1. ALWAYS use your tools to get data - NEVER ask the user for information you can retrieve -2. Call tools IMMEDIATELY when asked for financial metrics -3. Present results clearly after retrieving data -4. For date ranges: "Q1 2024" = 2024-01-01 to 2024-03-31, "2024" = 2024-01-01 to 2024-12-31 -5. Answer ONLY what was asked - don't provide extra reports unless requested - -TOOL SELECTION GUIDE: -- "runway" or "how long can we last" โ†’ Use runway tool -- "burn rate" or "monthly burn" โ†’ Use burnRate tool -- "revenue" or "income" โ†’ Use revenue tool -- "P&L" or "profit" or "loss" โ†’ Use profitLoss tool -- "cash flow" โ†’ Use cashFlow tool -- "balance sheet" or "assets/liabilities" โ†’ Use balanceSheet tool -- "expenses" or "spending breakdown" โ†’ Use expenses tool -- "tax" โ†’ Use taxSummary tool - -PRESENTATION STYLE: -- Reference the company name (${AGENT_CONTEXT.companyName}) when providing insights -- Use clear sections with headers for multiple metrics -- Include status indicators (e.g., "Status: Healthy", "Warning", "Critical") -- End with a brief key insight or takeaway when relevant -- Be concise but complete - no unnecessary fluff`, - tools: { - revenue: revenueDashboardTool, - profitLoss: profitLossTool, - cashFlow: cashFlowTool, - balanceSheet: balanceSheetTool, - expenses: expensesTool, - burnRate: burnRateMetricsTool, - runway: runwayMetricsTool, - spending: spendingMetricsTool, - taxSummary: taxSummaryTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - transactions: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a transactions specialist with access to live transaction data for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use your tools to get data - NEVER ask the user for transaction details -2. Call tools IMMEDIATELY when asked about transactions -3. For "largest transactions", use sort and limit filters -4. Present transaction data clearly in tables or lists - -PRESENTATION STYLE: -- Reference ${AGENT_CONTEXT.companyName} when relevant -- Use clear formatting (tables/lists) for multiple transactions -- Highlight key insights (e.g., "Largest expense: Marketing at 5,000 SEK") -- Be concise and data-focused`, - tools: { - listTransactions: listTransactionsTool, - getTransaction: getTransactionTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - invoices: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are an invoice management specialist for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use tools to get/create/update invoice data -2. Present invoice information clearly with key details (amount, status, due date) -3. Use clear status labels (Paid, Overdue, Pending)`, - tools: { - listInvoices: listInvoicesTool, - getInvoice: getInvoiceTool, - createInvoice: createInvoiceTool, - updateInvoice: updateInvoiceTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - timeTracking: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a time tracking specialist for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use tools to get/create/update time entries and timers -2. Present time data clearly (duration, project, date) -3. Summarize totals when showing multiple entries`, - tools: { - startTimer: startTimerTool, - stopTimer: stopTimerTool, - getTimeEntries: getTimeEntriesTool, - createTimeEntry: createTimeEntryTool, - updateTimeEntry: updateTimeEntryTool, - deleteTimeEntry: deleteTimeEntryTool, - getProjects: getTrackerProjectsTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - customers: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a customer management specialist for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use tools to get/create/update customer data -2. Present customer information clearly with key details -3. Highlight profitability insights when analyzing customers`, - tools: { - getCustomer: getCustomerTool, - createCustomer: createCustomerTool, - updateCustomer: updateCustomerTool, - profitabilityAnalysis: customerProfitabilityTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - analytics: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are an analytics & forecasting specialist with access to business intelligence tools for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use your tools to run analysis - NEVER ask user for data -2. Call tools IMMEDIATELY when asked for forecasts, health scores, or stress tests -3. Present analytics clearly with key insights highlighted -4. Answer ONLY what was asked - don't provide extra analysis unless requested - -TOOL SELECTION: -- "health" or "healthy" queries โ†’ Use businessHealth tool (gives consolidated score) -- "forecast" or "prediction" โ†’ Use cashFlowForecast tool -- "stress test" or "what if" โ†’ Use stressTest tool -- DO NOT call multiple detailed tools (revenue, P&L, etc.) - use businessHealth for overview - -PRESENTATION STYLE: -- Reference ${AGENT_CONTEXT.companyName} when providing insights -- Use clear trend labels (Increasing, Decreasing, Stable) -- Use clear status labels (Healthy, Warning, Critical) -- Include confidence levels when forecasting (e.g., "High confidence", "Moderate risk") -- End with 2-3 actionable focus areas (not a laundry list) -- Keep responses concise - quality over quantity`, - tools: { - businessHealth: businessHealthScoreTool, - cashFlowForecast: cashFlowForecastTool, - stressTest: cashFlowStressTestTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - operations: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are an operations specialist for ${AGENT_CONTEXT.companyName}. - -CRITICAL RULES: -1. ALWAYS use tools to get inbox items, documents, balances, or export data -2. Present information clearly with counts and summaries -3. Organize multiple items in clear lists or tables`, - tools: { - listInbox: listInboxItemsTool, - getBalances: getBalancesTool, - listDocuments: listDocumentsTool, - exportData: exportDataTool, - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - research: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a research specialist with access to real-time web search for ${AGENT_CONTEXT.companyName}. - -YOUR ROLE: -- Search the web for current information, news, and market data -- Find competitor information and industry insights -- Look up real-time data (exchange rates, stock prices, etc.) -- Research business tools, services, and best practices -- Verify facts and cross-reference information - -CRITICAL RULES: -1. ALWAYS use web_search when asked to find external information -2. Synthesize findings into actionable insights -3. Cite or reference sources when relevant -4. Distinguish between fact and opinion -5. Flag information gaps or uncertainties - -SCOPE: -โœ“ External information (news, competitors, markets, tools) -โœ— Internal company data (use appropriate specialists: reports, transactions, etc.) - -PRESENTATION STYLE: -- Provide clear summaries with key findings first -- Use bullet points for multiple findings -- Include context on how findings relate to ${AGENT_CONTEXT.companyName} -- Be concise but comprehensive -- Recommend next steps when relevant`, - tools: { - web_search: openai.tools.webSearch({ - searchContextSize: "high", - }), - }, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), - - general: new Agent({ - model: openai("gpt-4o-mini"), - system: `${getContextPrompt()} - -You are a general assistant for ${AGENT_CONTEXT.companyName}. - -YOUR ROLE: -- Handle general conversation (greetings, thanks, casual chat) -- Answer questions about what you can do and your capabilities -- Handle ambiguous or unclear requests by asking clarifying questions -- Provide helpful information about the available specialists - -AVAILABLE SPECIALISTS: -- **reports**: Financial metrics (revenue, P&L, burn rate, runway, etc.) -- **transactions**: Transaction history and details -- **invoices**: Invoice management -- **timeTracking**: Time tracking and timers -- **customers**: Customer management and profitability -- **analytics**: Forecasting and business intelligence -- **operations**: Inbox, documents, balances, data export -- **research**: Web search for external information, news, competitors, market data - -STYLE: -- Be friendly and helpful -- Reference ${AGENT_CONTEXT.companyName} when relevant -- If the user asks for something specific, suggest the right specialist -- Keep responses concise but complete`, - stopWhen: ({ steps }) => steps.length >= CONFIG.agents.maxSteps, - }), -}; - -// Orchestrator -const orchestrator = new Agent({ - model: openai("gpt-4o-mini"), - toolChoice: "required", - system: `${getContextPrompt()} - - -${RECOMMENDED_PROMPT_PREFIX} - -Route user requests to the appropriate agent: - -**reports**: Financial metrics and reports - - Revenue, P&L, expenses, spending - - Burn rate, runway (how long money will last) - - Cash flow, balance sheet, tax summary - -**transactions**: Transaction queries - - List transactions, search transactions - - Get specific transaction details - -**invoices**: Invoice management - - Create, update, list invoices - -**timeTracking**: Time tracking - - Start/stop timers, time entries -**customers**: Customer management - - Get/create/update customers, profitability analysis +export async function POST(request: NextRequest) { + const ip = getClientIP(request); + const { success, remaining } = await checkRateLimit(ip); -**analytics**: Advanced forecasting & analysis - - Business health score - - Cash flow forecasting (future predictions) - - Stress testing scenarios - -**operations**: Operations - - Inbox, balances, documents, exports - -**research**: Web search and external information - - Latest news, market trends, competitor research - - Real-time data lookup (exchange rates, stock prices, etc.) - - Industry insights, business tools research - - Any external information not in our internal systems - -**general**: General queries and conversation - - Greetings, thanks, casual conversation - - "What can you do?", "How does this work?" - - Memory queries: "What did I just ask?", "What did we discuss?" - - Ambiguous or unclear requests - - Default for anything that doesn't fit other specialists - -ROUTING RULES: -- "runway" = reports (not analytics) -- "forecast" = analytics (not reports) -- "search", "look up", "find", "latest news" = research (not reports) -- "what did I just ask" or memory queries = general -- Greetings, thanks, casual chat = general -- When uncertain = general (as default) -- Route to ONE specialist at a time`, - tools: { - handoff: tool({ - description: "Hand off to a specialist agent", - inputSchema: z.object({ - agent: z.enum([ - "reports", - "transactions", - "invoices", - "timeTracking", - "customers", - "analytics", - "operations", - "research", - "general", - ]), - reason: z.string().optional(), + if (!success) { + return new Response( + JSON.stringify({ + error: "Rate limit exceeded. Please try again later.", + remaining, }), - execute: async ({ agent, reason }) => ({ agent, reason }), - }), - }, -}); - -type HandoffData = { agent: keyof typeof specialists; reason?: string }; - -export async function POST(request: Request) { - // Rate limiting: 5 messages per IP per day - const clientIP = getClientIP(request); - const { success, limit, remaining, reset } = await checkRateLimit(clientIP); + { + status: 429, + headers: { "Content-Type": "application/json" }, + }, + ); + } + // Parse request body const { messages } = await request.json(); - const conversationMessages = convertToModelMessages(messages).slice(-8); // Only keep last 8 messages - - const response = createUIMessageStreamResponse({ - experimental_transform: smoothStream(), - stream: createUIMessageStream({ - execute: async ({ writer }) => { - // Handle rate limiting in the stream - if (!success) { - writer.write({ - type: "data-rate-limit", - data: { - code: "RATE_LIMIT_EXCEEDED", - limit, - remaining, - reset: new Date(reset).toISOString(), - }, - transient: true, - }); - - return; - } - - // Send rate limit info as data part - writer.write({ - type: "data-rate-limit", - data: { - code: "RATE_LIMIT_OK", - limit, - remaining, - reset: new Date(reset).toISOString(), - }, - transient: true, - }); - - // Set up artifact context with writer - setContext({ - writer, - userId: "demo-user", - fullName: "Demo User", - db: null, - user: { - teamId: "demo-team", - baseCurrency: "USD", - locale: "en-US", - fullName: "Demo User", - }, - }); - - // HYBRID ROUTING: Try programmatic classification first - const lastMessage = messages[messages.length - 1]; - const lastUserMessage = extractMessageText(lastMessage); - - console.log("[Route] Extracted content:", lastUserMessage); - const programmaticRoute = classifyIntent(lastUserMessage); - - let currentAgent: - | typeof orchestrator - | (typeof specialists)[keyof typeof specialists]; - - if (programmaticRoute && programmaticRoute in specialists) { - // Fast path: Direct to specialist (90% of cases) - currentAgent = specialists[programmaticRoute]; - console.log(`[Routing] Programmatic โ†’ ${programmaticRoute}`); - } else { - // Fallback: Use orchestrator for complex/ambiguous queries (10% of cases) - currentAgent = orchestrator; - console.log("[Routing] Fallback โ†’ orchestrator (ambiguous query)"); - } - - let round = 0; - const usedSpecialists = new Set(); - - // If we used programmatic routing, mark specialist as used - if (programmaticRoute && programmaticRoute in specialists) { - usedSpecialists.add(programmaticRoute); - } - - // Agent name lookup - WeakMap for O(1) performance - type AgentName = - | "orchestrator" - | "reports" - | "transactions" - | "invoices" - | "timeTracking" - | "customers" - | "analytics" - | "operations" - | "research" - | "general"; - const agentNames = new WeakMap([ - [orchestrator, "orchestrator"], - [specialists.reports, "reports"], - [specialists.transactions, "transactions"], - [specialists.invoices, "invoices"], - [specialists.timeTracking, "timeTracking"], - [specialists.customers, "customers"], - [specialists.analytics, "analytics"], - [specialists.operations, "operations"], - [specialists.research, "research"], - [specialists.general, "general"], - ]); - - while (round++ < CONFIG.orchestration.maxRounds) { - const agentName: AgentName = - agentNames.get(currentAgent) ?? "general"; - - // Send status: agent executing - writer.write({ - type: "data-agent-status", - data: { - status: "executing", - agent: agentName, - }, - transient: true, - }); - - // ๐ŸŽฏ CONTEXT STRATEGY - const messagesToSend = - currentAgent === orchestrator - ? [conversationMessages[conversationMessages.length - 1]] // Latest only - : conversationMessages.slice(-CONFIG.orchestration.contextWindow); // Recent context - - const result = currentAgent.stream({ - messages: messagesToSend, - }); - - // This automatically converts fullStream to proper UI message chunks - // Enable sendSources to include source-url parts for web search citations - const uiStream = result.toUIMessageStream({ - sendSources: true, - }); - - // Track for orchestration - let textAccumulated = ""; - let handoffData: HandoffData | null = null; - const toolCallNames = new Map(); // toolCallId -> toolName - let hasStartedContent = false; - - // Stream UI chunks - AI SDK handles all the formatting! - for await (const chunk of uiStream) { - // Clear status on first actual content (text or tool) - if ( - !hasStartedContent && - (chunk.type === "text-delta" || chunk.type === "tool-input-start") - ) { - writer.write({ - type: "data-agent-status", - data: { status: "completing", agent: agentName }, - transient: true, - }); - hasStartedContent = true; - } - - // Write chunk - type assertion needed because our custom AgentUIMessage - // type is more restrictive than the chunks from toUIMessageStream() - writer.write(chunk as any); - - // Track text for conversation history - if (chunk.type === "text-delta") { - textAccumulated += chunk.delta; - } - - // Track tool names when they start - if (chunk.type === "tool-input-start") { - toolCallNames.set(chunk.toolCallId, chunk.toolName); - } - - // Detect handoff from tool output - if (chunk.type === "tool-output-available") { - const toolName = toolCallNames.get(chunk.toolCallId); - if (toolName === "handoff") { - handoffData = chunk.output as HandoffData; - console.log("[Handoff Detected]", handoffData); - } - } - } - - // Update conversation - if (textAccumulated) { - conversationMessages.push({ - role: "assistant", - content: textAccumulated, - }); - } - - // Handle orchestration flow - if (currentAgent === orchestrator) { - if (handoffData) { - // Check if this specialist has already been used - if (usedSpecialists.has(handoffData.agent)) { - // Don't route to the same specialist twice - task is complete - break; - } - - // Send routing status - writer.write({ - type: "data-agent-status", - data: { - status: "routing", - agent: "orchestrator", - }, - transient: true, - }); - - // Mark specialist as used and route to it - usedSpecialists.add(handoffData.agent); - currentAgent = specialists[handoffData.agent]; - } else { - // Orchestrator done, no more handoffs - break; - } - } else { - // Specialist done - if (handoffData) { - // Specialist handed off to another specialist - if (usedSpecialists.has(handoffData.agent)) { - // Already used this specialist - complete - break; - } - - // Route to next specialist - usedSpecialists.add(handoffData.agent); - currentAgent = specialists[handoffData.agent]; - } else { - // No handoff - specialist is done, complete the task - break; - } - } - } - - writer.write({ type: "finish" }); - }, - }), + if (!messages || messages.length === 0) { + return new Response(JSON.stringify({ error: "No messages provided" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const appContext = buildAppContext({ + userId: "user-123", + fullName: "John Doe", + email: "john@acme.com", + teamId: "team-456", + companyName: "Acme Inc.", + baseCurrency: "USD", + locale: "en-US", + timezone: "America/New_York", }); - return response; + return triageAgent.toUIMessageStream({ + messages: convertToModelMessages(messages), + strategy: "auto", // Hybrid routing: programmatic + LLM fallback + maxRounds: 5, // Max agent handoffs + maxSteps: 10, // Max tool calls per agent + context: appContext, + experimental_transform: smoothStream({ chunking: "word" }), + sendReasoning: true, + onFinish: (options) => { + console.log("onFinish", options); + }, + onEvent: (event: AgentEvent) => { + // Log lifecycle events for debugging + console.log(`[AGENT EVENT] ${event.type}:`, { + agent: "agent" in event ? event.agent : undefined, + requestId: appContext.requestId, + }); + }, + }); } diff --git a/apps/example/src/types/agents.ts b/apps/example/src/types/agents.ts index 3cb0371..00d7497 100644 --- a/apps/example/src/types/agents.ts +++ b/apps/example/src/types/agents.ts @@ -1,5 +1,11 @@ -import type { UIMessage } from "ai"; +import type { + AgentDataParts, + AgentUIMessage as BaseAgentUIMessage, +} from "@ai-sdk-tools/agents"; +/** + * Extended agent status type with application-specific agent names + */ export type AgentStatus = { status: "routing" | "executing" | "completing"; agent: @@ -16,12 +22,32 @@ export type AgentStatus = { }; /** - * Custom UI Message type with agent orchestration status data + * Extended data parts interface with application-specific data + * + * This demonstrates how to extend the base AgentDataParts with + * custom data parts for your application. + */ +export interface AppDataParts extends AgentDataParts { + // Override the agent-status with our extended type + "agent-status": AgentStatus; +} + +/** + * Custom UI Message type with application-specific data parts + * + * This reuses the base AgentUIMessage from the agents package + * but extends it with application-specific data parts. + * + * @example Using in useChat + * ```typescript + * const { messages } = useChat({ + * api: '/api/chat', + * onData: (dataPart) => { + * if (dataPart.type === 'data-agent-status') { + * console.log('Agent:', dataPart.data.agent); + * } + * } + * }); + * ``` */ -export type AgentUIMessage = UIMessage< - never, // metadata type - { - // Agent status updates (transient - won't be in message history) - "agent-status": AgentStatus; - } // data parts type ->; +export type AgentUIMessage = BaseAgentUIMessage; diff --git a/apps/website/src/app/agents/page.tsx b/apps/website/src/app/agents/page.tsx new file mode 100644 index 0000000..ae18e52 --- /dev/null +++ b/apps/website/src/app/agents/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import AgentsContent from "@/components/agents-content"; + +export const metadata: Metadata = { + title: "AI SDK Agents - Multi-Agent Orchestration for AI Applications", + description: + "Build intelligent workflows with specialized agents, automatic handoffs, and seamless coordination. Works with any AI provider for complex multi-step tasks.", + keywords: [ + "AI agents", + "multi-agent systems", + "agent orchestration", + "AI workflows", + "agent handoffs", + "AI routing", + "specialized agents", + "AI SDK agents", + "intelligent agents", + "AI coordination", + ], + openGraph: { + title: "AI SDK Agents - Multi-Agent Orchestration for AI Applications", + description: + "Build intelligent workflows with specialized agents, automatic handoffs, and seamless coordination. Works with any AI provider.", + url: "https://ai-sdk-tools.dev/agents", + }, + twitter: { + title: "AI SDK Agents - Multi-Agent Orchestration for AI Applications", + description: + "Build intelligent workflows with specialized agents, automatic handoffs, and seamless coordination. Works with any AI provider.", + }, + alternates: { + canonical: "/agents", + }, +}; + +export default function AgentsPage() { + return ; +} diff --git a/apps/website/src/app/docs/agents/page.tsx b/apps/website/src/app/docs/agents/page.tsx new file mode 100644 index 0000000..362970c --- /dev/null +++ b/apps/website/src/app/docs/agents/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import AgentsContent from "@/components/docs/agents-docs-content"; + +export const metadata: Metadata = { + title: "Agents Documentation - AI SDK Tools", + description: + "Multi-agent orchestration for AI SDK v5. Build intelligent workflows with specialized agents, automatic handoffs, and seamless coordination. Works with any AI provider.", + keywords: [ + "AI agents", + "multi-agent systems", + "agent orchestration", + "AI workflows", + "agent handoffs", + "AI routing", + "specialized agents", + "AI SDK agents", + "agent coordination", + "intelligent agents", + ], + openGraph: { + title: "Agents Documentation - AI SDK Tools", + description: + "Multi-agent orchestration for AI SDK v5. Build intelligent workflows with specialized agents and automatic handoffs.", + url: "https://ai-sdk-tools.dev/docs/agents", + }, + twitter: { + title: "Agents Documentation - AI SDK Tools", + description: + "Multi-agent orchestration for AI SDK v5. Build intelligent workflows with specialized agents and automatic handoffs.", + }, + alternates: { + canonical: "/docs/agents", + }, +}; + +export default function AgentsPage() { + return ; +} diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index 512028e..debceb1 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -4,16 +4,19 @@ import DocsContent from "@/components/docs/docs-content"; export const metadata: Metadata = { title: "Documentation - AI SDK Tools", description: - "Complete documentation for AI SDK Tools. Learn how to build powerful AI applications with our enhanced tools for state management, debugging, and streaming interfaces.", + "Complete documentation for AI SDK Tools. Learn how to build powerful AI applications with our enhanced tools for multi-agent orchestration, state management, debugging, and streaming interfaces.", keywords: [ "AI SDK documentation", "AI tools documentation", + "AI agents documentation", + "multi-agent systems", "React AI state management", "AI debugging tools", "AI streaming interfaces", "AI SDK tools guide", "AI application development", "TypeScript AI tools", + "agent orchestration", ], openGraph: { title: "Documentation - AI SDK Tools", diff --git a/apps/website/src/app/globals.css b/apps/website/src/app/globals.css index c036ab5..434484d 100644 --- a/apps/website/src/app/globals.css +++ b/apps/website/src/app/globals.css @@ -23,7 +23,7 @@ } body { - background-color: #0f0f0f; + background-color: #0c0c0c; color: #d4d4d4; position: relative; } diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index 49e1964..e9b0c28 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -16,12 +16,14 @@ export const metadata: Metadata = { template: "%s | AI SDK Tools", }, description: - "Essential utilities that extend and improve the Vercel AI SDK experience. State management, debugging tools, and structured artifact streaming for building advanced AI interfaces.", + "Essential utilities that extend and improve the Vercel AI SDK experience. Multi-agent orchestration, state management, debugging tools, and structured artifact streaming for building advanced AI applications.", keywords: [ "AI SDK", "Vercel AI SDK", "React AI", "AI development tools", + "AI agents", + "multi-agent systems", "AI state management", "AI debugging", "AI artifacts", @@ -29,10 +31,12 @@ export const metadata: Metadata = { "AI applications", "AI development", "AI tools", + "AI SDK agents", "AI SDK store", "AI SDK devtools", "AI streaming", "AI components", + "agent orchestration", ], authors: [{ name: "AI SDK Tools Team" }], creator: "AI SDK Tools", diff --git a/apps/website/src/app/manifest.ts b/apps/website/src/app/manifest.ts index 4fcaa3b..0167edf 100644 --- a/apps/website/src/app/manifest.ts +++ b/apps/website/src/app/manifest.ts @@ -8,7 +8,7 @@ export default function manifest(): MetadataRoute.Manifest { "Essential utilities that extend and improve the Vercel AI SDK experience. State management, debugging tools, and structured artifact streaming for building advanced AI interfaces.", start_url: "/", display: "standalone", - background_color: "#0f0f0f", + background_color: "#0c0c0c", theme_color: "#1a1a1a", icons: [ { diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 4fc921c..f50a845 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -1,10 +1,9 @@ "use client"; import { useEffect, useState } from "react"; -import { highlight } from "sugar-high"; +import { CopyButton } from "@/components/copy-button"; import { DevtoolsDemo } from "@/components/devtools-demo"; import { LiveDemo } from "@/components/live-demo"; -import { CopyButton } from "@/components/copy-button"; export default function Home() { const [focusedDemo, setFocusedDemo] = useState<"store" | "devtools">("store"); @@ -29,16 +28,17 @@ export default function Home() {

Essential utilities that extend and improve the Vercel AI SDK - experience. State management, debugging tools, and structured - artifact streaming - everything you need to build advanced AI - interfaces beyond simple chat applications. + experience. Multi-agent orchestration, state management, debugging + tools, and structured artifact streaming - everything you need to + build advanced AI applications beyond simple chat interfaces.

{/* Package Grid */}
{[ + "@ai-sdk-tools/agents", "@ai-sdk-tools/store", - "@ai-sdk-tools/devtools", + "@ai-sdk-tools/devtools", "@ai-sdk-tools/artifacts", "@ai-sdk-tools/cache", ].map((pkg) => ( @@ -86,27 +86,16 @@ export default function Home() {
{/* Stacked Demos */} -
+
{/* Store Demo (LiveDemo) */}
-
+
โ—‡ AI SDK Store @@ -118,21 +107,13 @@ export default function Home() { {/* Devtools Demo */}
-
+
โ—‡ AI SDK Devtools @@ -146,6 +127,18 @@ export default function Home() { {/* Features Grid */}
+
+

+ Multi-Agent Orchestration +

+

+ Build intelligent workflows with specialized agents. Automatic + handoffs, programmatic routing, and seamless coordination across + any AI provider. Perfect for complex tasks requiring distinct + expertise. +

+
+

State Management

@@ -189,181 +182,10 @@ export default function Home() { applications.

- -
-

TypeScript First

-

- Full TypeScript support with intelligent autocompletion, type - safety, and comprehensive type definitions for all AI SDK tools. -

-
-
- {/* Package Examples */} -
-

Getting Started

- -
-
-
- โ—‡ Store (State Management) -
-
-
-      
-      
-    
- ) -} - -function MessageList() { - const messages = useChatMessages() - - return ( -
- {messages.map(msg => ( -
- {msg.content} -
- ))} -
- ) -}`), - }} - /> -
-
- -
-
- โ—‡ Devtools (Debugging) -
-
-
-      
-      
-      {process.env.NODE_ENV === 'development' && (
-        
-      )}
-    
- ) -} - -// Automatically tracks: -// - Tool calls -// - State changes -// - Performance metrics -// - Error handling`), - }} - /> -
-
- -
-
- โ—‡ Artifacts (Structured Streaming) -
-
-
-      

{data?.title}

- {status === 'loading' && ( -
Loading... {progress * 100}%
- )} - {data?.data.map(item => ( -
- {item.month}: \${item.burnRate} -
- ))} -
- ) -}`), - }} - /> -
-
- -
-
- โ—‡ Cache (Performance) -
-
-
 {
-    // Expensive API call
-    return await weatherAPI.get(location)
-  }
-})
-
-// Cache with one line
-const weatherTool = cached(expensiveWeatherTool)
-
-// First call: 2s API request
-// Next calls: <1ms from cache โšก
-
-// Works with streaming tools + artifacts
-const analysis = cached(burnRateAnalysis)
-// Caches complete data: yields + charts`),
-                  }}
-                />
-              
-
-
- - {/* Bottom CTA */}
@@ -385,12 +207,6 @@ const analysis = cached(burnRateAnalysis) > Quickstart โ†’ - - Store Docs โ†’ - +
+ {/* Hero */} + + + {/* Features Grid */} +
+
+

+ Automatic Agent Routing +

+

+ Pattern-based routing with regex and string matching. Route + requests instantly to the right specialist without LLM overhead. +

+
+ +
+

Seamless Handoffs

+

+ Agents can transfer control while preserving full conversation + context. Include handoff reasons and relevant data for smooth + transitions. +

+
+ +
+

+ Multi-Provider Support +

+

+ Use different AI models for different tasks. GPT-4 for analysis, + Claude for writing, Gemini for review - all in one workflow. +

+
+ +
+

Context-Aware Agents

+

+ Pass typed context to agents for team/user-specific behavior. + Dynamic instructions based on preferences, permissions, and state. +

+
+ +
+

Built-in Guardrails

+

+ Input/output validation, content moderation, and tool permissions. + Block, modify, or approve content before and after agent + execution. +

+
+ +
+

Production Ready

+

+ Built on AI SDK v5 with streaming, error handling, and + observability. Real-time event tracking for monitoring and + debugging. +

+
+
+ + {/* Use Cases */} +
+

Use Cases

+ +
+
+

Customer Support

+

+ Triage โ†’ Technical Support โ†’ Billing +

+

+ Route customer inquiries to specialized agents. Technical + questions to engineering support, billing to finance, general to + product. +

+
+ +
+

Content Creation

+

+ Research โ†’ Writing โ†’ Editing โ†’ Publishing +

+

+ Multi-stage content pipeline. Research agent gathers info, + writer creates content, editor reviews, publisher formats and + posts. +

+
+ +
+

Code Development

+

+ Planning โ†’ Implementation โ†’ Testing โ†’ Documentation +

+

+ Specialized agents for each phase. Architect plans, coder + implements, tester validates, documenter writes guides. +

+
+ +
+

Data Analysis

+

+ Collection โ†’ Processing โ†’ Visualization โ†’ Insights +

+

+ Agent pipeline for data workflows. Collector fetches data, + processor cleans, visualizer creates charts, analyst generates + insights. +

+
+
+
+ + {/* Code Comparison */} +
+

Implementation

+ +
+
+
+ โ—‡ Single Model Approach +
+
+
+              
+
+ +
+
+ โ—‡ Multi-Agent Approach +
+
+
+              
+
+
+
+ + {/* Bottom CTA */} +
+
+
+
+ git: (main)$ + + npm i @ai-sdk-tools/agents ai zod + +
+ +
+
+ +

+ Multi-agent orchestration for AI SDK v5. Works with any AI provider. +

+ +
+ + View Documentation โ†’ + +
+
+
+
+ ); +} diff --git a/apps/website/src/components/artifacts-content.tsx b/apps/website/src/components/artifacts-content.tsx index 6837ea4..c873462 100644 --- a/apps/website/src/components/artifacts-content.tsx +++ b/apps/website/src/components/artifacts-content.tsx @@ -90,7 +90,7 @@ export function ArtifactsContent() {
{/* Advanced UI Demo */} -
+
{demos[currentDemo].icon} {demos[currentDemo].title} @@ -431,8 +431,8 @@ function DashboardComponent() { npm i @ai-sdk-tools/artifacts @ai-sdk-tools/store -
diff --git a/apps/website/src/components/cache-content.tsx b/apps/website/src/components/cache-content.tsx index 04515ae..f4b2aa6 100644 --- a/apps/website/src/components/cache-content.tsx +++ b/apps/website/src/components/cache-content.tsx @@ -14,9 +14,10 @@ export function CacheContent() {

- Agents call the same tools repeatedly across conversation turns, burning money - and time. Cache expensive operations once, reuse instantly. Transform slow, - costly agent flows into lightning-fast experiences. + Agents call the same tools repeatedly across conversation turns, + burning money and time. Cache expensive operations once, reuse + instantly. Transform slow, costly agent flows into lightning-fast + experiences.

{/* Terminal */} @@ -77,7 +78,7 @@ export function CacheContent() {
{/* Code Demo */} -
+
โ—‡ Universal Tool Caching @@ -85,7 +86,8 @@ export function CacheContent() {
           
-

Universal Compatibility

+

+ Universal Compatibility +

- Works with any AI SDK tool - regular functions, streaming generators, - and complex artifact tools. One caching solution for all patterns. + Works with any AI SDK tool - regular functions, streaming + generators, and complex artifact tools. One caching solution for + all patterns.

-

Complete Data Preservation

+

+ Complete Data Preservation +

- Caches everything - return values, yielded chunks, and writer messages. - Streaming tools with artifacts work perfectly on cache hits. + Caches everything - return values, yielded chunks, and writer + messages. Streaming tools with artifacts work perfectly on cache + hits.

Zero Configuration

- Just wrap your tool with cached() and it works. React Query style + Just wrap your tool with cached() and it works. React Query style key generation, smart defaults, and automatic type inference.

@@ -144,24 +152,28 @@ const weatherTool = cached(expensiveWeatherTool)

Multiple Backends

- LRU cache for single instances, Redis for distributed apps. + LRU cache for single instances, Redis for distributed apps. Environment-aware configuration with seamless switching.

-

Production Performance

+

+ Production Performance +

- 10x faster responses for repeated requests. 80% cost reduction - by avoiding duplicate API calls and expensive computations. + 10x faster responses for repeated requests. 80% cost reduction by + avoiding duplicate API calls and expensive computations.

-

Agent Flow Optimization

+

+ Agent Flow Optimization +

- Agents naturally call the same tools across conversation turns. - Transform expensive repeated operations into instant responses for + Agents naturally call the same tools across conversation turns. + Transform expensive repeated operations into instant responses for smoother, faster, and cheaper agent experiences.

@@ -176,14 +188,13 @@ const weatherTool = cached(expensiveWeatherTool)
{/* Basic Usage */}
-
- โ—‡ Basic Usage -
+
โ—‡ Basic Usage
             

Start Caching Your AI Tools

- Reduce costs and improve performance with universal AI tool caching. + Reduce costs and improve performance with universal AI tool + caching.

{/* Used by */} @@ -210,8 +210,8 @@ function App() { git: (main)$ npm i @ai-sdk-tools/devtools
- diff --git a/apps/website/src/components/devtools-demo.tsx b/apps/website/src/components/devtools-demo.tsx index 4c9f5c8..d594165 100644 --- a/apps/website/src/components/devtools-demo.tsx +++ b/apps/website/src/components/devtools-demo.tsx @@ -134,7 +134,7 @@ export function DevtoolsDemo() { }; return ( -
+
{/* Header */}
โ—‡ AI SDK Devtools
@@ -171,7 +171,7 @@ export function DevtoolsDemo() { toolCalls.map((call) => (
{call.name} @@ -207,7 +207,7 @@ export function DevtoolsDemo() { {mockMetrics.map((metric) => (
@@ -233,7 +233,7 @@ export function DevtoolsDemo() { {selectedTab === "logs" && (
Application Logs
-
+
[INFO] AI SDK Devtools initialized diff --git a/apps/website/src/components/docs/agents-docs-content.tsx b/apps/website/src/components/docs/agents-docs-content.tsx new file mode 100644 index 0000000..a926327 --- /dev/null +++ b/apps/website/src/components/docs/agents-docs-content.tsx @@ -0,0 +1,798 @@ +"use client"; + +import Link from "next/link"; +import { highlight } from "sugar-high"; +import { CopyButton } from "../copy-button"; + +export default function AgentsDocsContent() { + return ( +
+
+ {/* Hero */} +
+
+

+ Agents +

+

+ Multi-agent orchestration for AI SDK v5. Build intelligent + workflows with specialized agents, automatic handoffs, and + seamless coordination. Works with any AI provider. +

+ +
+ + npm i @ai-sdk-tools/agents ai zod + + +
+
+
+ + {/* Why Multi-Agent Systems */} +
+
+

+ Why Multi-Agent Systems? +

+

+ Complex tasks benefit from specialized expertise. Instead of a + single model handling everything, break work into focused agents: +

+
+
+

Customer Support

+

+ Triage โ†’ Technical Support โ†’ Billing +

+
+
+

Content Pipeline

+

+ Research โ†’ Writing โ†’ Editing โ†’ Publishing +

+
+
+

Code Development

+

+ Planning โ†’ Implementation โ†’ Testing โ†’ Documentation +

+
+
+

Data Analysis

+

+ Collection โ†’ Processing โ†’ Visualization โ†’ Insights +

+
+
+
+
+ โ€ข +
+

Specialization

+

+ Each agent focuses on its domain with optimized instructions + and tools +

+
+
+
+ โ€ข +
+

+ Context Preservation +

+

+ Full conversation history maintained across handoffs +

+
+
+
+ โ€ข +
+

+ Provider Flexibility +

+

+ Use different models for different tasks (GPT-4 for + analysis, Claude for writing) +

+
+
+
+ โ€ข +
+

+ Programmatic Routing +

+

+ Pattern matching and automatic agent selection +

+
+
+
+
+
+ + {/* Quick Start */} +
+
+

Quick Start

+
+
+

+ Basic: Single Agent +

+
+
+                
+
+ +
+

+ Handoffs: Two Specialists +

+
+
+                
+
+ +
+

+ Orchestration: Auto-Routing +

+

+ Use programmatic routing for instant agent selection without + LLM overhead: +

+
+
+                
+
+
+
+
+ + {/* Streaming with UI */} +
+
+

Streaming with UI

+

+ For Next.js route handlers and real-time UI updates: +

+
+
 {
+      if (event.type === 'agent-handoff') {
+        console.log(\`Handoff: \${event.from} โ†’ \${event.to}\`)
+      }
+    },
+  })
+}`),
+                }}
+                suppressHydrationWarning
+              />
+            
+
+
+ + {/* Tools and Context */} +
+
+

Tools and Context

+
+
+

Adding Tools

+
+
 {
+    return eval(expression) // Use safe-eval in production
+  },
+})
+
+const agent = new Agent({
+  name: 'Calculator Agent',
+  model: openai('gpt-4o'),
+  instructions: 'Help with math using the calculator tool.',
+  tools: {
+    calculator: calculatorTool,
+  },
+  maxTurns: 20, // Max tool call iterations
+})`),
+                    }}
+                    suppressHydrationWarning
+                  />
+                
+
+ +
+

+ Context-Aware Agents +

+

+ Use typed context for team/user-specific behavior: +

+
+
+}
+
+const agent = new Agent({
+  name: 'Team Assistant',
+  model: openai('gpt-4o'),
+  instructions: (context) => {
+    return \`You are helping team \${context.teamId}. 
+    User preferences: \${JSON.stringify(context.preferences)}\`
+  },
+})
+
+// Pass context when streaming
+agent.toUIMessageStream({
+  messages,
+  context: {
+    teamId: 'team-123',
+    userId: 'user-456',
+    preferences: { theme: 'dark', language: 'en' },
+  },
+})`),
+                    }}
+                    suppressHydrationWarning
+                  />
+                
+
+
+
+
+ + {/* Multi-Provider Setup */} +
+
+

Multi-Provider Setup

+

+ Use the best model for each task: +

+
+
+            
+
+
+ + {/* Guardrails */} +
+
+

Guardrails

+

+ Control agent behavior with input/output validation: +

+
+
 {
+      if (containsProfanity(input)) {
+        return { 
+          pass: false, 
+          action: 'block',
+          message: 'Input violates content policy',
+        }
+      }
+      return { pass: true }
+    },
+  ],
+  outputGuardrails: [
+    async (output) => {
+      if (containsSensitiveInfo(output)) {
+        return { 
+          pass: false, 
+          action: 'modify',
+          modifiedOutput: redactSensitiveInfo(output),
+        }
+      }
+      return { pass: true }
+    },
+  ],
+})`),
+                }}
+                suppressHydrationWarning
+              />
+            
+
+
+ + {/* API Reference */} +
+
+

API Reference

+ +
+
+

+ Agent Constructor Options +

+
+
+ name: string - Unique agent + identifier +
+
+ model: LanguageModel - AI + SDK language model +
+
+ + instructions: string | ((context: TContext) => string) + {" "} + - System prompt +
+
+ + tools?: Record<string, Tool> + {" "} + - Available tools +
+
+ handoffs?: Agent[] - Agents + this agent can hand off to +
+
+ maxTurns?: number - Maximum + tool call iterations (default: 10) +
+
+ temperature?: number - + Model temperature +
+
+ + matchOn?: (string | RegExp)[] | ((message: string) => + boolean) + {" "} + - Routing patterns +
+
+ + onEvent?: (event: AgentEvent) => void + {" "} + - Lifecycle event handler +
+
+ + inputGuardrails?: InputGuardrail[] + {" "} + - Pre-execution validation +
+
+ + outputGuardrails?: OutputGuardrail[] + {" "} + - Post-execution validation +
+
+ + permissions?: ToolPermissions + {" "} + - Tool access control +
+
+
+ +
+

Methods

+
+
+ + generate(options) + +

+ Generate response (non-streaming) +

+
+
+ + stream(options) + +

+ Stream response (AI SDK stream) +

+
+
+ + toUIMessageStream(options) + +

+ Stream as UI messages (Next.js route handler) +

+
+
+ + getHandoffs() + +

+ Get handoff agents +

+
+
+
+ +
+

Event Types

+
+
+ โ€ข agent-start - Agent starts execution +
+
+ โ€ข agent-step - Agent completes a step +
+
+ โ€ข agent-finish - Agent finishes round +
+
+ โ€ข agent-handoff - Agent hands off to another +
+
+ โ€ข agent-complete - All execution complete +
+
+ โ€ข agent-error - Error occurred +
+
+
+
+
+
+ + {/* Integration with Other Packages */} +
+
+

+ Integration with Other Packages +

+ +
+
+

+ With @ai-sdk-tools/cache +

+

+ Cache expensive tool calls across agents: +

+
+
+                
+
+ +
+

+ With @ai-sdk-tools/artifacts +

+

+ Stream structured artifacts from agents: +

+
+
+                
+
+ +
+

+ With @ai-sdk-tools/devtools +

+

+ Debug agent execution in development: +

+
+
 {
+    console.log('[Agent Event]', event)
+  },
+})
+
+// In your app
+export default function App() {
+  return (
+    <>
+      
+      
+    
+  )
+}`),
+                    }}
+                    suppressHydrationWarning
+                  />
+                
+
+
+
+
+ + {/* Examples */} +
+
+

Examples

+

+ Real-world implementations can be found in{" "} + /apps/example/src/ai/agents/: +

+
+
+ โ€ข +
+

Triage Agent

+

+ Route customer questions to specialists +

+
+
+
+ โ€ข +
+

Financial Agent

+

+ Multi-step analysis with artifacts +

+
+
+
+ โ€ข +
+

Code Review

+

+ Analyze โ†’ Test โ†’ Document workflow +

+
+
+
+ โ€ข +
+

Multi-Provider

+

+ Use different models for different tasks +

+
+
+
+
+
+ + {/* Next Steps */} +
+
+

Next Steps

+
+ +

+ View on GitHub +

+

+ Explore source code and contribute +

+ + +

+ Back to Documentation +

+

Explore other packages

+ +
+
+
+
+
+ ); +} diff --git a/apps/website/src/components/docs/docs-content.tsx b/apps/website/src/components/docs/docs-content.tsx index e4edefc..8a2354c 100644 --- a/apps/website/src/components/docs/docs-content.tsx +++ b/apps/website/src/components/docs/docs-content.tsx @@ -84,6 +84,23 @@ export default function DocsContent() {

Packages

+ +

+ @ai-sdk-tools/agents +

+

+ Multi-agent orchestration with automatic handoffs and routing. + Build intelligent workflows with specialized agents for any AI + provider. +

+
+ Learn more โ†’ +
+ + + +

+ @ai-sdk-tools/cache +

+

+ Universal caching for AI SDK tools. Cache expensive operations + with zero configuration - works with regular tools, streaming, + and artifacts. +

+
+ Learn more โ†’ +
+ + Individual Packages
+
+

+ Multi-Agent Orchestration +

+

+ Build intelligent workflows with specialized agents and + automatic handoffs. +

+
+ + npm i @ai-sdk-tools/agents ai zod + + +
+
+

State Management

@@ -115,6 +150,34 @@ export default function InstallationContent() {

+ +
+

Universal Caching

+

+ Cache expensive AI tool executions with zero configuration. +

+
+ npm i @ai-sdk-tools/cache + +
+
@@ -130,18 +193,19 @@ export default function InstallationContent() {
- npm i @ai-sdk-tools/store @ai-sdk-tools/devtools - @ai-sdk-tools/artifacts + npm i @ai-sdk-tools/agents @ai-sdk-tools/store + @ai-sdk-tools/devtools @ai-sdk-tools/artifacts + @ai-sdk-tools/cache ai zod
-
โ”” Watch selectors update live
+
+ โ”” Watch selectors update live +
- ) + ); } diff --git a/bun.lock b/bun.lock index 06b1411..b5e93c1 100644 --- a/bun.lock +++ b/bun.lock @@ -204,7 +204,7 @@ "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.35", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11", "@vercel/oidc": "3.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cdsXbeRRMi6QxbZscin69Asx2fi0d2TmmPngcPFUMpZbchGEBiJYVNvIfiALKFKXEq0l/w0xGNV3E13vroaleA=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9OsVWzW502UCYN1VS9D+1flwrF9GqFvpfybfb1iLIdvmCbXXKXpozhLyuAW82FY67hfiIlOsvlgT9UYtljlQmw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3FHZdiTLbjnHw0rbu1yOPW8FruHrzN6SlJYsaLSQgbxYfE5y+60Nj4Xp8/k7rtD3FmrjkKcp/XTMSbAJWfoJig=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -832,7 +832,7 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "ai": ["ai@5.0.63", "", { "dependencies": { "@ai-sdk/gateway": "1.0.35", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rQB8xSCDCZLvAue51hvufhSP82NtzZh8CMH7/Oj5rnn/5vNI2y+Rn0lmODJ/kC4bPvFX8XgDfys8i4r86YfkmQ=="], + "ai": ["ai@5.0.64", "", { "dependencies": { "@ai-sdk/gateway": "1.0.35", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-a7H1z2Xz6NQdgx+FIdDlkenoPYBbxbmJSbRfnOFnYS1S1XraiHT8M85hLvz8d8zlxVtSSjiP+c4EjqwtAe72cg=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -1770,12 +1770,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk-tools/devtools/ai": ["ai@5.0.64", "", { "dependencies": { "@ai-sdk/gateway": "1.0.35", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-a7H1z2Xz6NQdgx+FIdDlkenoPYBbxbmJSbRfnOFnYS1S1XraiHT8M85hLvz8d8zlxVtSSjiP+c4EjqwtAe72cg=="], - - "@ai-sdk-tools/website/@ai-sdk/openai": ["@ai-sdk/openai@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3FHZdiTLbjnHw0rbu1yOPW8FruHrzN6SlJYsaLSQgbxYfE5y+60Nj4Xp8/k7rtD3FmrjkKcp/XTMSbAJWfoJig=="], - - "@ai-sdk-tools/website/ai": ["ai@5.0.64", "", { "dependencies": { "@ai-sdk/gateway": "1.0.35", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.11", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-a7H1z2Xz6NQdgx+FIdDlkenoPYBbxbmJSbRfnOFnYS1S1XraiHT8M85hLvz8d8zlxVtSSjiP+c4EjqwtAe72cg=="], - "@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], "@ai-sdk/react/ai": ["ai@5.0.60", "", { "dependencies": { "@ai-sdk/gateway": "1.0.33", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-80U/3kmdBW6g+JkLXpz/P2EwkyEaWlPlYtuLUpx/JYK9F7WZh9NnkYoh1KvUi1Sbpo0NyurBTvX0a2AG9mmbDA=="], diff --git a/image.png b/image.png index fcce451..5df0898 100644 Binary files a/image.png and b/image.png differ diff --git a/packages/agents/README.md b/packages/agents/README.md index 599fef8..378749e 100644 --- a/packages/agents/README.md +++ b/packages/agents/README.md @@ -2,39 +2,83 @@ [![npm version](https://badge.fury.io/js/@ai-sdk-tools%2Fagents.svg)](https://badge.fury.io/js/@ai-sdk-tools%2Fagents) -Multi-agent orchestration system built on AI SDK v5. Create intelligent agent workflows with handoffs, routing, and coordination - works with any AI provider. +Multi-agent orchestration for AI SDK v5. Build intelligent workflows with specialized agents, automatic handoffs, and seamless coordination. Works with any AI provider. -## โšก Why Multi-Agent Systems? +```bash +npm install @ai-sdk-tools/agents ai zod +``` -Complex tasks often require specialized expertise: -- **Customer Support**: Triage โ†’ Technical Support โ†’ Billing -- **Content Creation**: Research โ†’ Writing โ†’ Editing โ†’ Review -- **Data Analysis**: Collection โ†’ Processing โ†’ Visualization โ†’ Insights -- **Code Development**: Planning โ†’ Implementation โ†’ Testing โ†’ Documentation +## Why Multi-Agent Systems? -Benefits: -- **๐ŸŽฏ Specialized Agents** - Each agent focuses on what it does best -- **๐Ÿ”„ Seamless Handoffs** - Intelligent transfer between agents with context preservation -- **๐ŸŒ Provider Agnostic** - Works with OpenAI, Anthropic, Google, Meta, xAI, and more -- **๐Ÿ“Š Full Traceability** - Complete execution traces and debugging -- **โšก Built on AI SDK v5** - Leverages the proven foundation with enhanced orchestration +Complex tasks benefit from specialized expertise. Instead of a single model handling everything, break work into focused agents: -## Installation +**Customer Support**: Triage โ†’ Technical Support โ†’ Billing +**Content Pipeline**: Research โ†’ Writing โ†’ Editing โ†’ Publishing +**Code Development**: Planning โ†’ Implementation โ†’ Testing โ†’ Documentation +**Data Analysis**: Collection โ†’ Processing โ†’ Visualization โ†’ Insights -```bash -npm install @ai-sdk-tools/agents ai zod -# or -bun add @ai-sdk-tools/agents ai zod -``` +### Benefits + +- **Specialization** - Each agent focuses on its domain with optimized instructions and tools +- **Context Preservation** - Full conversation history maintained across handoffs +- **Provider Flexibility** - Use different models for different tasks (GPT-4 for analysis, Claude for writing) +- **Programmatic Routing** - Pattern matching and automatic agent selection +- **Production Ready** - Built on AI SDK v5 with streaming, error handling, and observability + +### When to Use Agents + +**Use multi-agent systems when:** +- Tasks require distinct expertise (technical vs. creative vs. analytical) +- Workflow has clear stages that could be handled independently +- Different models excel at different parts of the task +- You need better control over specialized behavior + +**Use single model when:** +- Task is straightforward and can be handled by general instructions +- No clear separation of concerns +- Response time is critical (multi-agent adds orchestration overhead) + +## Core Concepts + +### Agent +An AI with specialized instructions, tools, and optional context. Each agent is configured with a language model and system prompt tailored to its role. + +### Handoffs +Agents can transfer control to other agents while preserving conversation context. Handoffs include the reason for transfer and any relevant context. + +### Orchestration +Automatic routing between agents based on: +- **Programmatic matching**: Pattern-based routing with `matchOn` +- **LLM-based routing**: The orchestrator agent decides which specialist to invoke +- **Hybrid**: Combine both for optimal performance ## Quick Start -### Basic Multi-Agent Setup +### Basic: Single Agent ```typescript -import { Agent, run } from '@ai-sdk-tools/agents'; -import { openai } from 'ai'; -import { z } from 'zod'; +import { Agent } from '@ai-sdk-tools/agents'; +import { openai } from '@ai-sdk/openai'; + +const agent = new Agent({ + name: 'Assistant', + model: openai('gpt-4o'), + instructions: 'You are a helpful assistant.', +}); + +// Generate response +const result = await agent.generate({ + prompt: 'What is 2+2?', +}); + +console.log(result.text); // "4" +``` + +### Handoffs: Two Specialists + +```typescript +import { Agent } from '@ai-sdk-tools/agents'; +import { openai } from '@ai-sdk/openai'; // Create specialized agents const mathAgent = new Agent({ @@ -44,154 +88,257 @@ const mathAgent = new Agent({ }); const historyAgent = new Agent({ - name: 'History Tutor', + name: 'History Tutor', model: openai('gpt-4o'), instructions: 'You help with history questions. Provide context and dates.', }); -// Create triage agent with handoffs -const triageAgent = new Agent({ - name: 'Triage Agent', +// Create orchestrator with handoff capability +const orchestrator = new Agent({ + name: 'Triage', model: openai('gpt-4o'), - instructions: 'Route student questions to the appropriate tutor specialist.', + instructions: 'Route questions to the appropriate specialist.', handoffs: [mathAgent, historyAgent], }); -// Run the conversation -const result = await run(triageAgent, 'What is 2+2 and when was the Civil War?'); -console.log(result.response); -console.log(`Final agent: ${result.finalAgent}`); -console.log(`Handoffs: ${result.handoffs.length}`); +// LLM decides which specialist to use +const result = await orchestrator.generate({ + prompt: 'What is the quadratic formula?', +}); + +console.log(`Handled by: ${result.finalAgent}`); // "Math Tutor" +console.log(`Handoffs: ${result.handoffs.length}`); // 1 ``` -### Multi-Provider Example +### Orchestration: Auto-Routing + +Use programmatic routing for instant agent selection without LLM overhead: ```typescript -import { openai } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; -import { google } from '@ai-sdk/google'; +const mathAgent = new Agent({ + name: 'Math Tutor', + model: openai('gpt-4o'), + instructions: 'You help with math problems.', + matchOn: ['calculate', 'math', 'equation', /\d+\s*[\+\-\*\/]\s*\d+/], +}); -// Use different models for different agents -const researchAgent = new Agent({ - name: 'Researcher', - model: anthropic('claude-3-sonnet-20240229'), // Great for analysis - instructions: 'Research topics thoroughly and gather comprehensive information.', +const historyAgent = new Agent({ + name: 'History Tutor', + model: openai('gpt-4o'), + instructions: 'You help with history questions.', + matchOn: ['history', 'war', 'civilization', /\d{4}/], // Years }); -const writerAgent = new Agent({ - name: 'Writer', - model: openai('gpt-4o'), // Great for creative writing - instructions: 'Create engaging, well-structured content based on research.', +const orchestrator = new Agent({ + name: 'Smart Router', + model: openai('gpt-4o-mini'), // Efficient for routing + instructions: 'Route to specialists. Fall back to handling general questions.', + handoffs: [mathAgent, historyAgent], }); -const editorAgent = new Agent({ - name: 'Editor', - model: google('gemini-1.5-pro'), // Great for review and editing - instructions: 'Review and improve content for clarity and accuracy.', - handoffs: [writerAgent], // Can send back for rewrites +// Automatically routes to mathAgent based on pattern match +const result = await orchestrator.generate({ + prompt: 'What is 15 * 23?', }); +``` -const contentPipeline = new Agent({ - name: 'Content Manager', - model: openai('gpt-4o-mini'), // Efficient for routing - instructions: 'Coordinate content creation from research to final publication.', - handoffs: [researchAgent, writerAgent, editorAgent], +## Advanced Patterns + +### Streaming with UI + +For Next.js route handlers and real-time UI updates: + +```typescript +// app/api/chat/route.ts +import { Agent } from '@ai-sdk-tools/agents'; +import { openai } from '@ai-sdk/openai'; + +const supportAgent = new Agent({ + name: 'Support', + model: openai('gpt-4o'), + instructions: 'Handle customer support inquiries.', + handoffs: [technicalAgent, billingAgent], }); + +export async function POST(req: Request) { + const { messages } = await req.json(); + + return supportAgent.toUIMessageStream({ + messages, + maxRounds: 5, // Max handoffs + maxSteps: 10, // Max tool calls per agent + onEvent: async (event) => { + if (event.type === 'agent-handoff') { + console.log(`Handoff: ${event.from} โ†’ ${event.to}`); + } + }, + }); +} ``` -### With Tools and Context +### Tools and Context ```typescript import { tool } from 'ai'; +import { z } from 'zod'; -// Create tools for agents const calculatorTool = tool({ - description: 'Perform mathematical calculations', + description: 'Perform calculations', parameters: z.object({ expression: z.string(), }), execute: async ({ expression }) => { - // Safe eval implementation - return eval(expression); + return eval(expression); // Use safe-eval in production }, }); -const searchTool = tool({ - description: 'Search for information', - parameters: z.object({ - query: z.string(), - }), - execute: async ({ query }) => { - // Your search implementation - return `Search results for: ${query}`; +const agent = new Agent({ + name: 'Calculator Agent', + model: openai('gpt-4o'), + instructions: 'Help with math using the calculator tool.', + tools: { + calculator: calculatorTool, }, + maxTurns: 20, // Max tool call iterations }); +``` -// Agent with tools -const assistantAgent = new Agent({ - name: 'Assistant', +### Context-Aware Agents + +Use typed context for team/user-specific behavior: + +```typescript +interface TeamContext { + teamId: string; + userId: string; + preferences: Record; +} + +const agent = new Agent({ + name: 'Team Assistant', model: openai('gpt-4o'), - instructions: 'Help users with calculations and information lookup.', - tools: { - calculator: calculatorTool, - search: searchTool, + instructions: (context) => { + return `You are helping team ${context.teamId}. + User preferences: ${JSON.stringify(context.preferences)}`; }, }); -// Run with custom options -const result = await run(assistantAgent, 'Calculate 15% of 200 and search for tips', { - maxTotalTurns: 20, - onHandoff: (handoff) => console.log(`Handing off to: ${handoff.targetAgent}`), - onToolCall: (toolCall) => console.log(`Tool called: ${toolCall.name}`), +// Pass context when streaming +agent.toUIMessageStream({ + messages, + context: { + teamId: 'team-123', + userId: 'user-456', + preferences: { theme: 'dark', language: 'en' }, + }, +}); +``` + +### Custom Routing Function + +```typescript +const expertAgent = new Agent({ + name: 'Expert', + model: openai('gpt-4o'), + instructions: 'Handle complex technical questions.', + matchOn: (message) => { + const complexity = calculateComplexity(message); + return complexity > 0.7; + }, }); ``` -## Advanced Usage +### Multi-Provider Setup -### Router-Based Agent Selection +Use the best model for each task: ```typescript -import { createTriageAgent, Runner } from '@ai-sdk-tools/agents'; +import { openai } from '@ai-sdk/openai'; +import { anthropic } from '@ai-sdk/anthropic'; +import { google } from '@ai-sdk/google'; -// Create specialized agents -const agents = [mathAgent, historyAgent, scienceAgent]; +const researchAgent = new Agent({ + name: 'Researcher', + model: anthropic('claude-3-5-sonnet-20241022'), // Excellent reasoning + instructions: 'Research topics thoroughly.', +}); -// Create intelligent router -const router = createTriageAgent('Smart Router', agents, mathAgent); +const writerAgent = new Agent({ + name: 'Writer', + model: openai('gpt-4o'), // Great at creative writing + instructions: 'Create engaging content.', +}); -// Use with runner for more control -const runner = new Runner({ - maxTotalTurns: 100, - enableTracing: true, +const editorAgent = new Agent({ + name: 'Editor', + model: google('gemini-1.5-pro'), // Strong at review + instructions: 'Review and improve content.', + handoffs: [writerAgent], // Can send back for rewrites }); -runner.registerAgents(agents); -const result = await runner.run(router, 'Explain photosynthesis'); +const pipeline = new Agent({ + name: 'Content Manager', + model: openai('gpt-4o-mini'), // Efficient orchestrator + instructions: 'Coordinate content creation.', + handoffs: [researchAgent, writerAgent, editorAgent], +}); ``` -### Custom Handoff Logic +### Guardrails + +Control agent behavior with input/output validation: ```typescript -import { createHandoff } from '@ai-sdk-tools/agents'; +const agent = new Agent({ + name: 'Moderated Agent', + model: openai('gpt-4o'), + instructions: 'Answer questions helpfully.', + inputGuardrails: [ + async (input) => { + if (containsProfanity(input)) { + return { + pass: false, + action: 'block', + message: 'Input violates content policy', + }; + } + return { pass: true }; + }, + ], + outputGuardrails: [ + async (output) => { + if (containsSensitiveInfo(output)) { + return { + pass: false, + action: 'modify', + modifiedOutput: redactSensitiveInfo(output), + }; + } + return { pass: true }; + }, + ], +}); +``` + +### Tool Permissions -const supervisorAgent = new Agent({ - name: 'Supervisor', +Control which tools agents can access: + +```typescript +const agent = new Agent({ + name: 'Restricted Agent', model: openai('gpt-4o'), - instructions: 'Oversee task completion and quality.', + instructions: 'Help with tasks.', tools: { - escalate: tool({ - description: 'Escalate to human supervisor', - parameters: z.object({ - reason: z.string(), - priority: z.enum(['low', 'medium', 'high']), - }), - execute: async ({ reason, priority }) => { - if (priority === 'high') { - return createHandoff('human-supervisor', reason, 'High priority escalation'); - } - return 'Handled internally'; - }, - }), + readData: readDataTool, + writeData: writeDataTool, + deleteData: deleteDataTool, + }, + permissions: { + allowed: ['readData', 'writeData'], // deleteData blocked + maxCallsPerTool: { + writeData: 5, // Limit writes + }, }, }); ``` @@ -201,62 +348,130 @@ const supervisorAgent = new Agent({ ### Agent Class ```typescript -class Agent { - constructor(config: AgentConfig) - async run(input: string, context?: AgentContext): Promise - static create(config: AgentConfig): Agent -} +class Agent = Record> ``` -### Runner Class +**Constructor Options:** +- `name: string` - Unique agent identifier +- `model: LanguageModel` - AI SDK language model +- `instructions: string | ((context: TContext) => string)` - System prompt +- `tools?: Record` - Available tools +- `handoffs?: Agent[]` - Agents this agent can hand off to +- `maxTurns?: number` - Maximum tool call iterations (default: 10) +- `temperature?: number` - Model temperature +- `matchOn?: (string | RegExp)[] | ((message: string) => boolean)` - Routing patterns +- `onEvent?: (event: AgentEvent) => void` - Lifecycle event handler +- `inputGuardrails?: InputGuardrail[]` - Pre-execution validation +- `outputGuardrails?: OutputGuardrail[]` - Post-execution validation +- `permissions?: ToolPermissions` - Tool access control + +**Methods:** ```typescript -class Runner { - constructor(options?: RunOptions) - registerAgent(agent: Agent): void - registerAgents(agents: Agent[]): void - async run(agent: Agent | string, input: string): Promise -} +// Generate response (non-streaming) +async generate(options: { + prompt: string; + messages?: ModelMessage[]; +}): Promise + +// Stream response (AI SDK stream) +stream(options: { + prompt?: string; + messages?: ModelMessage[]; +}): AgentStreamResult + +// Stream as UI messages (Next.js route handler) +toUIMessageStream(options: { + messages: ModelMessage[]; + strategy?: 'auto' | 'manual'; + maxRounds?: number; + maxSteps?: number; + context?: TContext; + onEvent?: (event: AgentEvent) => void; + beforeStream?: (ctx: { writer: UIMessageStreamWriter }) => boolean | Promise; + // ... AI SDK stream options +}): Response + +// Get handoff agents +getHandoffs(): Agent[] ``` ### Utility Functions ```typescript -// Simple execution -async function run(agent: Agent, input: string, options?: RunOptions): Promise +// Create handoff instruction +createHandoff( + targetAgent: string, + context?: string, + reason?: string +): HandoffInstruction + +// Check if result is handoff +isHandoffResult(result: unknown): result is HandoffInstruction + +// Create handoff tool for AI SDK +createHandoffTool(agents: Agent[]): Tool + +// Execution context +createExecutionContext(options: { + context?: T; + writer?: UIMessageStreamWriter; + metadata?: Record; +}): ExecutionContext + +// Routing utilities +matchAgent(message: string, agents: Agent[]): Agent | null +findBestMatch(message: string, agents: Agent[]): Agent | null + +// Streaming utilities +writeAgentStatus(writer: UIMessageStreamWriter, status: { + status: 'executing' | 'routing' | 'completing'; + agent: string; +}): void +``` -// Handoff utilities -function createHandoff(targetAgent: Agent | string, context?: string, reason?: string): HandoffInstruction -function createHandoffTool(availableAgents: Agent[]): Tool -function isHandoffResult(result: unknown): result is HandoffInstruction +### Event Types -// Router creation -function createRouter(config: RouterConfig): RouterAgent -function createTriageAgent(name: string, agents: Agent[], defaultAgent?: Agent): RouterAgent +```typescript +type AgentEvent = + | { type: 'agent-start'; agent: string; round: number } + | { type: 'agent-step'; agent: string; step: StepResult } + | { type: 'agent-finish'; agent: string; round: number } + | { type: 'agent-handoff'; from: string; to: string; reason?: string } + | { type: 'agent-complete'; totalRounds: number } + | { type: 'agent-error'; error: Error } ``` ## Integration with Other Packages ### With @ai-sdk-tools/cache +Cache expensive tool calls across agents: + ```typescript import { createCached } from '@ai-sdk-tools/cache'; +import { Redis } from '@upstash/redis'; -const cached = createCached(); -const expensiveAgent = new Agent({ - name: 'Data Analyst', +const cached = createCached({ cache: Redis.fromEnv() }); + +const agent = new Agent({ + name: 'Data Agent', model: openai('gpt-4o'), - instructions: 'Perform complex data analysis.', + instructions: 'Analyze data.', tools: { - analyze: cached(expensiveAnalysisTool), // Cache expensive operations + analyze: cached(expensiveAnalysisTool), }, }); ``` ### With @ai-sdk-tools/artifacts +Stream structured artifacts from agents: + ```typescript import { artifact } from '@ai-sdk-tools/artifacts'; +import { tool } from 'ai'; +import { z } from 'zod'; const ReportArtifact = artifact('report', z.object({ title: z.string(), @@ -269,37 +484,68 @@ const ReportArtifact = artifact('report', z.object({ const reportAgent = new Agent({ name: 'Report Generator', model: openai('gpt-4o'), - instructions: 'Generate structured reports with real-time updates.', + instructions: 'Generate structured reports.', tools: { - updateReport: tool({ - description: 'Update report with new section', + createReport: tool({ + description: 'Create a report', parameters: z.object({ - section: z.object({ - heading: z.string(), - content: z.string(), - }), + title: z.string(), }), - execute: async ({ section }) => { - const report = ReportArtifact.stream(); - await report.update({ sections: [section] }); - return 'Section added'; + execute: async function* ({ title }) { + const report = ReportArtifact.stream({ title, sections: [] }); + + yield { text: 'Generating report...' }; + + await report.update({ + sections: [{ heading: 'Introduction', content: '...' }], + }); + + yield { text: 'Report complete', forceStop: true }; }, }), }, }); ``` +### With @ai-sdk-tools/devtools + +Debug agent execution in development: + +```typescript +import { AIDevTools } from '@ai-sdk-tools/devtools'; + +const agent = new Agent({ + name: 'Debug Agent', + model: openai('gpt-4o'), + instructions: 'Test agent.', + onEvent: (event) => { + console.log('[Agent Event]', event); + }, +}); + +// In your app +export default function App() { + return ( + <> + + + + ); +} +``` + ## Examples -Check out the `/examples` directory for complete implementations: -- **Customer Support Pipeline** - Triage โ†’ Technical โ†’ Billing -- **Content Creation Workflow** - Research โ†’ Write โ†’ Edit โ†’ Publish -- **Code Review System** - Analyze โ†’ Test โ†’ Document โ†’ Deploy -- **Multi-Provider Setup** - Using different models for different tasks +Real-world implementations in `/apps/example/src/ai/agents/`: + +- **Triage Agent** - Route customer questions to specialists +- **Financial Agent** - Multi-step analysis with artifacts +- **Code Review** - Analyze โ†’ Test โ†’ Document workflow +- **Multi-Provider** - Use different models for different tasks ## Contributing -We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details. +Contributions are welcome! See the [contributing guide](../../CONTRIBUTING.md) for details. ## License diff --git a/packages/agents/src/agent.ts b/packages/agents/src/agent.ts index e6719b7..e17ae22 100644 --- a/packages/agents/src/agent.ts +++ b/packages/agents/src/agent.ts @@ -3,32 +3,55 @@ import { createUIMessageStream, createUIMessageStreamResponse, type ModelMessage, + type StepResult, stepCountIs, type Tool, } from "ai"; import { debug } from "./debug.js"; import { createHandoffTool, isHandoffResult } from "./handoff.js"; import { promptWithHandoffInstructions } from "./handoff-prompt.js"; -import { run } from "./runner.js"; +import { writeAgentStatus } from "./streaming.js"; import type { AgentConfig, + AgentEvent, AgentGenerateOptions, AgentGenerateResult, AgentStreamOptions, + AgentStreamOptionsUI, AgentStreamResult, HandoffInstruction, + Agent as IAgent, + InputGuardrail, + OutputGuardrail, + ToolPermissions, } from "./types.js"; +import { extractTextFromMessage } from "./utils.js"; -export class Agent { +export class Agent< + TContext extends Record = Record, +> implements IAgent +{ public readonly name: string; - public readonly instructions: string; + public readonly instructions: string | ((context: TContext) => string); + public readonly matchOn?: + | (string | RegExp)[] + | ((message: string) => boolean); + public readonly onEvent?: (event: AgentEvent) => void | Promise; + public readonly inputGuardrails?: InputGuardrail[]; + public readonly outputGuardrails?: OutputGuardrail[]; + public readonly permissions?: ToolPermissions; private readonly aiAgent: AISDKAgent>; - private readonly handoffAgents: Agent[]; + private readonly handoffAgents: Array>; - constructor(config: AgentConfig) { + constructor(config: AgentConfig) { this.name = config.name; this.instructions = config.instructions; - this.handoffAgents = (config.handoffs as Agent[]) || []; + this.matchOn = config.matchOn; + this.onEvent = config.onEvent; + this.inputGuardrails = config.inputGuardrails; + this.outputGuardrails = config.outputGuardrails; + this.permissions = config.permissions; + this.handoffAgents = config.handoffs || []; // Prepare tools with handoff capability const tools = { ...config.tools }; @@ -36,11 +59,14 @@ export class Agent { tools.handoff_to_agent = createHandoffTool(this.handoffAgents); } - // Add recommended prompt prefix for handoffs + // Note: If instructions is a function, it will be resolved per-call in stream() + // We still need to create the AI SDK Agent with initial instructions for backwards compatibility + const baseInstructions = + typeof config.instructions === "string" ? config.instructions : ""; const systemPrompt = this.handoffAgents.length > 0 - ? promptWithHandoffInstructions(config.instructions) - : config.instructions; + ? promptWithHandoffInstructions(baseInstructions) + : baseInstructions; // Create AI SDK Agent this.aiAgent = new AISDKAgent>({ @@ -57,7 +83,6 @@ export class Agent { const startTime = new Date(); try { - // Use AI SDK Agent's generate method const result = options.messages && options.messages.length > 0 ? await this.aiAgent.generate({ @@ -87,7 +112,7 @@ export class Agent { } return { - ...result, + text: result.text || "", finalAgent: this.name, finalOutput: result.text || "", handoffs, @@ -96,6 +121,14 @@ export class Agent { endTime, duration: endTime.getTime() - startTime.getTime(), }, + steps: result.steps, + finishReason: result.finishReason, + usage: result.usage, + toolCalls: result.toolCalls?.map((tc) => ({ + toolCallId: tc.toolCallId, + toolName: tc.toolName, + args: "args" in tc ? tc.args : undefined, + })), }; } catch (error) { throw new Error( @@ -104,210 +137,417 @@ export class Agent { } } - async stream(options: AgentStreamOptions): Promise { + stream( + options: AgentStreamOptions | { messages: ModelMessage[] }, + ): AgentStreamResult { debug("STREAM", `${this.name} stream called`); + + // Extract our internal execution context (we map to/from AI SDK's experimental_context at boundaries) + const executionContext = (options as Record) + .executionContext as Record | undefined; + const maxSteps = (options as Record).maxSteps as + | number + | undefined; + const onStepFinish = (options as Record).onStepFinish as + | ((step: unknown) => void | Promise) + | undefined; + + // Resolve instructions dynamically (static string or function) + const resolvedInstructions = + typeof this.instructions === "function" + ? this.instructions(executionContext as TContext) + : this.instructions; + + // Add handoff instructions if needed + const systemPrompt = + this.handoffAgents.length > 0 + ? promptWithHandoffInstructions(resolvedInstructions) + : resolvedInstructions; + + // Build additional options to pass to AI SDK + const additionalOptions: Record = { + system: systemPrompt, // Override system prompt per call + }; + if (executionContext) + additionalOptions.experimental_context = executionContext; + if (maxSteps) additionalOptions.maxSteps = maxSteps; + if (onStepFinish) additionalOptions.onStepFinish = onStepFinish; + + // Handle simple { messages } format (like working code) + if ("messages" in options && !("prompt" in options) && options.messages) { + debug("ORCHESTRATION", `Stream with messages only`, { + messageCount: options.messages.length, + }); + return this.aiAgent.stream({ + messages: options.messages, + ...additionalOptions, + } as any) as AgentStreamResult; + } + + // Handle full AgentStreamOptions format + const opts = options as AgentStreamOptions; debug("ORCHESTRATION", `Stream options for ${this.name}`, { - hasPrompt: !!options.prompt, - messageCount: options.messages?.length || 0, + hasPrompt: !!opts.prompt, + messageCount: opts.messages?.length || 0, }); - if ( - !options.prompt && - (!options.messages || options.messages.length === 0) - ) { + if (!opts.prompt && (!opts.messages || opts.messages.length === 0)) { throw new Error("No prompt or messages provided to stream method"); } - // If we have messages, we need to use a different approach - if (options.messages && options.messages.length > 0) { + // If we have messages, append prompt as user message + if (opts.messages && opts.messages.length > 0 && opts.prompt) { return this.aiAgent.stream({ - messages: [ - ...options.messages, - { role: "user", content: options.prompt || "Continue" }, - ], - }); + messages: [...opts.messages, { role: "user", content: opts.prompt }], + ...additionalOptions, + } as any) as AgentStreamResult; } - return this.aiAgent.stream({ - prompt: options.prompt, - }); + // Prompt only + if (opts.prompt) { + return this.aiAgent.stream({ + prompt: opts.prompt, + ...additionalOptions, + } as any) as AgentStreamResult; + } + + throw new Error("No valid options provided to stream method"); } - getHandoffs(): Agent[] { + getHandoffs(): Array> { return this.handoffAgents; } - // Respond method - uses orchestration for handoffs, native AI SDK for single agents - async respond(options: { messages: ModelMessage[] }) { - // If this agent has handoffs, use orchestration - if (this.handoffAgents.length > 0) { - debug("ORCHESTRATION", `Starting orchestration with ${this.name}`, { - handoffAgents: this.handoffAgents.map((a) => a.name), - messageCount: options.messages.length, - }); + /** + * Convert agent execution to UI Message Stream Response + * High-level API for Next.js route handlers + * + * This follows the working pattern from the route.ts reference code + */ + toUIMessageStream(options: AgentStreamOptionsUI): Response { + const { + messages, + strategy = "auto", + maxRounds = 5, + maxSteps = 5, + context, + beforeStream, + onEvent, + // AI SDK createUIMessageStream options + onFinish, + onError, + generateId, + // AI SDK toUIMessageStream options + sendReasoning, + sendSources, + sendFinish, + sendStart, + messageMetadata, + // Response options + status, + statusText, + headers, + } = options; + + // Extract input from last message for routing + const lastMessage = messages[messages.length - 1]; + const input = extractTextFromMessage(lastMessage); + + const stream = createUIMessageStream({ + originalMessages: messages as never[], + onFinish, + onError, + generateId, + execute: async ({ writer }) => { + // Import context utilities + const { createExecutionContext } = await import("./context.js"); + + // Create execution context with user context and writer + const executionContext = createExecutionContext({ + context: (context || {}) as Record, + writer, + metadata: { + agent: this.name, + requestId: `req_${Date.now()}_${Math.random().toString(36).substring(7)}`, + }, + }); + + try { + // Execute beforeStream hook - allows for rate limiting, auth, etc. + if (beforeStream) { + const shouldContinue = await beforeStream({ writer }); + if (shouldContinue === false) { + writer.write({ type: "finish" } as any); + return; + } + } - // Extract text from last message - const lastMessage = options.messages[options.messages.length - 1] as any; - - // Handle both content and parts structure - let textContent = ""; - if (lastMessage.parts) { - // Message has parts structure - const textPart = lastMessage.parts.find( - (part: any) => part.type === "text", - ); - textContent = textPart?.text || ""; - } else if (Array.isArray(lastMessage.content)) { - // Message has content array - const textPart = lastMessage.content.find( - (part: any) => part.type === "text", - ); - textContent = textPart?.text || ""; - } else { - // Message has direct content - textContent = lastMessage.content || ""; - } + // Prepare conversation messages + const conversationMessages = [...messages]; - debug( - "ORCHESTRATION", - `Extracted user input: "${textContent.substring(0, 100)}..."`, - ); + // Get handoff agents (specialists) + const specialists = this.getHandoffs(); - // Messages are already in ModelMessage format from the API route - - // Create UI message stream using proper AI SDK v5 pattern - const stream = createUIMessageStream({ - execute: async ({ writer }) => { - const startTime = Date.now(); - debug( - "STREAM", - "Starting orchestration stream with full message history", - ); - - // Generate message ID for text deltas - const messageId = `orchestration-${Date.now()}`; - - try { - // Use orchestration runner with full message history for proper handoff support - // Pass all messages except the last one (which is the current user message that will be the prompt) - const previousMessages = - options.messages.length > 1 - ? options.messages.slice(0, -1) - : undefined; - - debug("STREAM", "Starting orchestration with message history:", { - prompt: `${textContent.substring(0, 50)}...`, - messageCount: previousMessages?.length || 0, - hasHandoffs: this.handoffAgents.length > 0, - totalMessages: options.messages.length, + // Determine starting agent using programmatic routing + let currentAgent: IAgent = this; + + if (strategy === "auto" && specialists.length > 0) { + // Try programmatic classification + const matchedAgent = specialists.find((agent) => { + if (!agent.matchOn) return false; + if (typeof agent.matchOn === "function") { + return agent.matchOn(input); + } + if (Array.isArray(agent.matchOn)) { + return agent.matchOn.some((pattern) => { + if (typeof pattern === "string") { + return input.toLowerCase().includes(pattern.toLowerCase()); + } + if (pattern instanceof RegExp) { + return pattern.test(input); + } + return false; + }); + } + return false; }); - const result = await run(this, textContent, { - stream: true, - maxTotalTurns: 15, - initialMessages: previousMessages, + if (matchedAgent) { + currentAgent = matchedAgent; + console.log(`[ROUTING] Programmatic match: ${currentAgent.name}`); + } + } + + let round = 0; + const usedSpecialists = new Set(); + + // If we used programmatic routing, mark specialist as used + if (currentAgent !== this) { + usedSpecialists.add(currentAgent.name); + } + + while (round++ < maxRounds) { + // Send status: agent executing + writeAgentStatus(writer, { + status: "executing", + agent: currentAgent.name, }); - // Directly write to writer instead of using merge - try { - let isFirstTextChunk = true; - - for await (const chunk of result.stream) { - if (chunk.type === "text-delta") { - // Send text-start for the first text chunk - if (isFirstTextChunk) { - writer.write({ - type: "text-start", - id: messageId, - }); - isFirstTextChunk = false; - } + const messagesToSend = + currentAgent === this + ? [conversationMessages[conversationMessages.length - 1]] // Latest only + : conversationMessages.slice(-8); // Recent context + + // Emit agent start event + if (onEvent) { + await onEvent({ + type: "agent-start", + agent: currentAgent.name, + round, + }); + } - writer.write({ - type: "text-delta", - id: messageId, - delta: chunk.text, - }); - } else if (chunk.type === "tool-call") { - debug( - "TOOL", - `${chunk.agent} using ${chunk.toolName}`, - chunk.args, - ); - writer.write({ - type: "text-delta", - id: messageId, - delta: `\n๐Ÿ”ง Using ${chunk.toolName}...\n`, - }); - } else if (chunk.type === "tool-result") { - debug( - "TOOL", - `${chunk.agent} completed ${chunk.toolName}`, - chunk.result, - ); - writer.write({ - type: "text-delta", - id: messageId, - delta: `โœ… ${chunk.toolName}: ${typeof chunk.result === "string" ? chunk.result : JSON.stringify(chunk.result)}\n`, - }); - } else if (chunk.type === "agent-switch") { - debug("HANDOFF", `${chunk.agent} โ†’ ${chunk.toAgent}`, { - context: chunk.context, - reason: chunk.reason, - }); - writer.write({ - type: "text-delta", - id: messageId, - delta: `\n๐Ÿ”„ Switching to ${chunk.toAgent}${chunk.reason ? ` (${chunk.reason})` : ""}...\n`, - }); - } else if (chunk.type === "agent-complete") { - debug("COMPLETE", `${chunk.agent} completed`, { - outputLength: chunk.finalOutput?.length || 0, + const result = currentAgent.stream({ + messages: messagesToSend, + executionContext: executionContext, + maxSteps, // Limit tool calls per round + onStepFinish: async (step: unknown) => { + if (onEvent) { + await onEvent({ + type: "agent-step", + agent: currentAgent.name, + step: step as StepResult>, }); - // Don't write finalOutput - we already streamed all text-delta chunks + } + }, + } as any); + + // This automatically converts fullStream to proper UI message chunks + // Pass toUIMessageStream options from user config + const uiStream = result.toUIMessageStream({ + sendReasoning, + sendSources, + sendFinish, + sendStart, + messageMetadata, + }); + + // Track for orchestration + let textAccumulated = ""; + let handoffData: any = null; + const toolCallNames = new Map(); // toolCallId -> toolName + let hasStartedContent = false; + + // Stream UI chunks - AI SDK handles all the formatting! + for await (const chunk of uiStream) { + // Clear status on first actual content (text or tool) + if ( + !hasStartedContent && + (chunk.type === "text-delta" || + chunk.type === "tool-input-start") + ) { + writeAgentStatus(writer, { + status: "completing", + agent: currentAgent.name, + }); + hasStartedContent = true; + } + + // Write chunk - type assertion needed because our custom AgentUIMessage + // type is more restrictive than the chunks from toUIMessageStream() + writer.write(chunk as any); + + // Track text for conversation history + if (chunk.type === "text-delta") { + textAccumulated += chunk.delta; + } + + // Track tool names when they start + if (chunk.type === "tool-input-start") { + toolCallNames.set(chunk.toolCallId, chunk.toolName); + } + + // Detect handoff from tool output + if (chunk.type === "tool-output-available") { + const toolName = toolCallNames.get(chunk.toolCallId); + if (toolName === "handoff") { + handoffData = chunk.output; + console.log("[Handoff Detected]", handoffData); } } + } - // End the text message - writer.write({ - type: "text-end", - id: messageId, + // Update conversation + if (textAccumulated) { + conversationMessages.push({ + role: "assistant", + content: textAccumulated, }); - } catch (error) { - console.error("DEBUG: Error in orchestration stream:", error); - throw error; } - // Wait for orchestration to complete - const finalResult = await result.result; - const duration = Date.now() - startTime; - debug("PERF", `Orchestration completed in ${duration}ms`, { - finalAgent: finalResult.finalAgent, - handoffCount: finalResult.handoffs?.length || 0, - }); - } catch (error) { - debug("ERROR", "Orchestration failed", error); - writer.write({ - type: "text-delta", - id: messageId, - delta: `\nโŒ Error: ${error instanceof Error ? error.message : "Unknown error"}\n`, + // Emit agent finish event + if (onEvent) { + await onEvent({ + type: "agent-finish", + agent: currentAgent.name, + round, + }); + } + + // Handle orchestration flow + if (currentAgent === this) { + if (handoffData) { + // Check if this specialist has already been used + if (usedSpecialists.has(handoffData.agent)) { + // Don't route to the same specialist twice - task is complete + break; + } + + // Send routing status + writeAgentStatus(writer, { + status: "routing", + agent: "orchestrator", + }); + + // Mark specialist as used and route to it + usedSpecialists.add(handoffData.agent); + const nextAgent = specialists.find( + (a) => a.name === handoffData.agent, + ); + if (nextAgent) { + currentAgent = nextAgent; + + // Emit handoff event + if (onEvent) { + await onEvent({ + type: "agent-handoff", + from: this.name, + to: nextAgent.name, + reason: handoffData.reason, + }); + } + } + } else { + // Orchestrator done, no more handoffs + break; + } + } else { + // Specialist done + if (handoffData) { + // Specialist handed off to another specialist + if (usedSpecialists.has(handoffData.agent)) { + // Already used this specialist - complete + break; + } + + // Route to next specialist + usedSpecialists.add(handoffData.agent); + const nextAgent = specialists.find( + (a) => a.name === handoffData.agent, + ); + if (nextAgent) { + const previousAgent = currentAgent; + currentAgent = nextAgent; + + // Emit handoff event + if (onEvent) { + await onEvent({ + type: "agent-handoff", + from: previousAgent.name, + to: nextAgent.name, + reason: handoffData.reason, + }); + } + } + } else { + // No handoff - specialist is done, complete the task + break; + } + } + } + + // Emit completion event + if (onEvent) { + await onEvent({ + type: "agent-complete", + totalRounds: round, }); - writer.write({ - type: "text-end", - id: messageId, + } + + writer.write({ type: "finish" }); + } catch (error) { + console.error("[AGENT] Error in toUIMessageStream:", error); + + // Emit error event + if (onEvent) { + await onEvent({ + type: "agent-error", + error: error instanceof Error ? error : new Error(String(error)), }); } - }, - }); - return createUIMessageStreamResponse({ stream }); - } + writer.write({ + type: "error", + error: error instanceof Error ? error.message : String(error), + } as any); + writer.write({ type: "finish" } as any); + } + }, + }); + + const response = createUIMessageStreamResponse({ + stream, + status, + statusText, + headers, + }); - // For single agents, use native AI SDK - return this.aiAgent.respond(options as any); + return response; } - static create(config: AgentConfig): Agent { - return new Agent(config); + static create< + TContext extends Record = Record, + >(config: AgentConfig): Agent { + return new Agent(config); } } diff --git a/packages/agents/src/context.ts b/packages/agents/src/context.ts new file mode 100644 index 0000000..2e69510 --- /dev/null +++ b/packages/agents/src/context.ts @@ -0,0 +1,158 @@ +/** + * Context Management using AI SDK's experimental_context + * + * This provides type-safe context that flows through tools via AI SDK's + * built-in experimental_context parameter. + * + * Key features: + * - Uses AI SDK's official context mechanism + * - Fully flexible user context - pass ANY type you want (object, string, class instance, etc.) + * - Stream writer for artifacts and real-time updates + * - Type-safe with full TypeScript support + * - Available in all tools via executionOptions.experimental_context + */ + +import type { UIMessageStreamWriter } from "ai"; + +/** + * Core execution context that flows through tools via AI SDK + * + * This merges your custom context with required system fields. + * Your context fields are available at the top level alongside writer and metadata. + * + * @template TContext - Your custom context type (must be an object) + */ +export type ExecutionContext< + TContext extends Record = Record, +> = TContext & { + /** Stream writer for real-time updates and artifacts */ + writer: UIMessageStreamWriter; + + /** Metadata about the current execution */ + metadata?: { + /** Current agent name */ + agent?: string; + /** Execution start time */ + startTime?: Date; + /** Request ID for tracing */ + requestId?: string; + /** Any custom metadata */ + [key: string]: unknown; + }; +}; + +/** + * Type-safe context creator options + * + * @template TContext - Your custom context type (must be an object) + */ +export interface ContextOptions< + TContext extends Record = Record, +> { + /** Your custom application context - spread at the top level */ + context: TContext; + + /** Stream writer (required in streaming mode) */ + writer: UIMessageStreamWriter; + + /** Metadata */ + metadata?: ExecutionContext["metadata"]; +} + +/** + * Create an execution context to pass to AI SDK's experimental_context + * + * Your context object is spread at the top level, merged with writer and metadata. + * This means you can access your fields directly without a wrapper. + * + * @template TContext - Your custom context type (must be an object) + * + * @example Basic usage + * ```typescript + * const context = createExecutionContext({ + * context: { userId: '123', db: database, permissions: ['read', 'write'] }, + * writer: streamWriter + * }); + * // Access in tools: executionOptions.experimental_context.userId + * ``` + * + * @example With typed context + * ```typescript + * interface MyAppContext { + * tenant: string; + * workspace: string; + * features: string[]; + * } + * + * const context = createExecutionContext({ + * context: { tenant: 'acme', workspace: 'main', features: ['analytics'] }, + * writer: streamWriter + * }); + * // Access in tools: executionOptions.experimental_context.tenant + * ``` + * + * @example With metadata + * ```typescript + * const context = createExecutionContext({ + * context: { userId: '123', tenantId: 'acme' }, + * writer: streamWriter, + * metadata: { agent: 'reports', requestId: 'req_123' } + * }); + * ``` + */ +export function createExecutionContext< + TContext extends Record = Record, +>(options: ContextOptions): ExecutionContext { + return { + ...options.context, + writer: options.writer, + metadata: { + startTime: new Date(), + ...options.metadata, + }, + } as ExecutionContext; +} + +/** + * Get your custom context from execution options + * + * Your context fields are available directly in experimental_context (no wrapper). + * This helper provides type-safe access. + * + * @template T - Your custom context type (object) + * @param executionOptions - Tool execution options from AI SDK + * @returns Your custom context + * + * @example Direct access (no helper needed) + * ```typescript + * export const myTool = tool({ + * execute: async (params, executionOptions) => { + * // Access fields directly + * const userId = executionOptions.experimental_context.userId; + * const db = executionOptions.experimental_context.db; + * } + * }); + * ``` + * + * @example With typed helper + * ```typescript + * interface AppContext { + * userId: string; + * tenantId: string; + * db: Database; + * } + * + * export const myTool = tool({ + * execute: async (params, executionOptions) => { + * const { userId, tenantId, db } = getContext(executionOptions); + * const user = await db.users.findOne(userId); + * } + * }); + * ``` + */ +export function getContext< + T extends Record = Record, +>(executionOptions?: { experimental_context?: T }): T | undefined { + // AI SDK passes context via experimental_context + return executionOptions?.experimental_context; +} diff --git a/packages/agents/src/guardrails.ts b/packages/agents/src/guardrails.ts new file mode 100644 index 0000000..334aa3c --- /dev/null +++ b/packages/agents/src/guardrails.ts @@ -0,0 +1,190 @@ +/** + * Guardrails for AI Agents + * + * Following OpenAI Agents SDK pattern for input/output validation + * https://openai.github.io/openai-agents-js/guides/guardrails/ + */ + +import type { InputGuardrail, OutputGuardrail } from "./types.js"; + +/** + * Base error class for all agent errors + */ +export class AgentsError extends Error { + constructor( + message: string, + public readonly state?: unknown, + ) { + super(message); + this.name = "AgentsError"; + } +} + +/** + * Error thrown when input guardrail tripwire is triggered + */ +export class InputGuardrailTripwireTriggered extends AgentsError { + constructor( + public readonly guardrailName: string, + public readonly outputInfo?: unknown, + state?: unknown, + ) { + super(`Input guardrail tripwire triggered: ${guardrailName}`, state); + this.name = "InputGuardrailTripwireTriggered"; + } +} + +/** + * Error thrown when output guardrail tripwire is triggered + */ +export class OutputGuardrailTripwireTriggered extends AgentsError { + constructor( + public readonly guardrailName: string, + public readonly outputInfo?: unknown, + state?: unknown, + ) { + super(`Output guardrail tripwire triggered: ${guardrailName}`, state); + this.name = "OutputGuardrailTripwireTriggered"; + } +} + +/** + * Error thrown when a guardrail fails to execute + */ +export class GuardrailExecutionError extends AgentsError { + constructor( + public readonly guardrailName: string, + public readonly originalError: Error, + state?: unknown, + ) { + super( + `Guardrail execution failed: ${guardrailName} - ${originalError.message}`, + state, + ); + this.name = "GuardrailExecutionError"; + } +} + +/** + * Error thrown when maximum turns are exceeded + */ +export class MaxTurnsExceededError extends AgentsError { + constructor( + public readonly currentTurns: number, + public readonly maxTurns: number, + state?: unknown, + ) { + super(`Maximum turns exceeded: ${currentTurns}/${maxTurns}`, state); + this.name = "MaxTurnsExceededError"; + } +} + +/** + * Error thrown when a tool call fails + */ +export class ToolCallError extends AgentsError { + constructor( + public readonly toolName: string, + public readonly originalError: Error, + state?: unknown, + ) { + super(`Tool call failed: ${toolName} - ${originalError.message}`, state); + this.name = "ToolCallError"; + } +} + +/** + * Error thrown for tool permission denial + */ +export class ToolPermissionDeniedError extends AgentsError { + constructor( + public readonly toolName: string, + public readonly reason: string, + state?: unknown, + ) { + super(`Tool permission denied: ${toolName} - ${reason}`, state); + this.name = "ToolPermissionDeniedError"; + } +} + +/** + * Run input guardrails in parallel + */ +export async function runInputGuardrails( + guardrails: InputGuardrail[], + input: string, + context?: unknown, +): Promise { + if (!guardrails || guardrails.length === 0) return; + + const results = await Promise.allSettled( + guardrails.map(async (guardrail) => { + try { + const result = await guardrail.execute({ input, context }); + if (result.tripwireTriggered) { + throw new InputGuardrailTripwireTriggered( + guardrail.name, + result.outputInfo, + ); + } + return result; + } catch (error) { + if (error instanceof InputGuardrailTripwireTriggered) { + throw error; + } + throw new GuardrailExecutionError( + guardrail.name, + error instanceof Error ? error : new Error(String(error)), + ); + } + }), + ); + + // Check for failures + for (const result of results) { + if (result.status === "rejected") { + throw result.reason; + } + } +} + +/** + * Run output guardrails in parallel + */ +export async function runOutputGuardrails( + guardrails: OutputGuardrail[], + agentOutput: TOutput, + context?: unknown, +): Promise { + if (!guardrails || guardrails.length === 0) return; + + const results = await Promise.allSettled( + guardrails.map(async (guardrail) => { + try { + const result = await guardrail.execute({ agentOutput, context }); + if (result.tripwireTriggered) { + throw new OutputGuardrailTripwireTriggered( + guardrail.name, + result.outputInfo, + ); + } + return result; + } catch (error) { + if (error instanceof OutputGuardrailTripwireTriggered) { + throw error; + } + throw new GuardrailExecutionError( + guardrail.name, + error instanceof Error ? error : new Error(String(error)), + ); + } + }), + ); + + // Check for failures + for (const result of results) { + if (result.status === "rejected") { + throw result.reason; + } + } +} diff --git a/packages/agents/src/index.ts b/packages/agents/src/index.ts index 81d2bf9..1124097 100644 --- a/packages/agents/src/index.ts +++ b/packages/agents/src/index.ts @@ -1,22 +1,59 @@ -// Core exports (matches OpenAI Agents SDK API) +// Core exports export { Agent } from "./agent.js"; +export type { ContextOptions, ExecutionContext } from "./context.js"; +// Context management +export { createExecutionContext, getContext } from "./context.js"; +// Guardrails +export { + AgentsError, + GuardrailExecutionError, + InputGuardrailTripwireTriggered, + MaxTurnsExceededError, + OutputGuardrailTripwireTriggered, + runInputGuardrails, + runOutputGuardrails, + ToolCallError, + ToolPermissionDeniedError, +} from "./guardrails.js"; // Handoff utilities export { createHandoff, createHandoffTool, isHandoffResult, } from "./handoff.js"; -export { Runner, run, runStream } from "./runner.js"; - +// Permissions +export { + checkToolPermission, + createUsageTracker, + trackToolCall, +} from "./permissions.js"; +// Routing +export { findBestMatch, matchAgent } from "./routing.js"; +// Streaming utilities +export { + writeAgentStatus, + writeDataPart, + writeRateLimit, +} from "./streaming.js"; // Types export type { AgentConfig, + AgentDataParts, + AgentEvent, AgentGenerateOptions, AgentGenerateResult, - AgentStreamingResult, AgentStreamOptions, + AgentStreamOptionsUI, AgentStreamResult, + AgentUIMessage, + GuardrailResult, HandoffInstruction, - RunOptions, - StreamChunk, + InputGuardrail, + OutputGuardrail, + ToolPermissionCheck, + ToolPermissionContext, + ToolPermissionResult, + ToolPermissions, } from "./types.js"; +// Utilities +export { extractTextFromMessage } from "./utils.js"; diff --git a/packages/agents/src/permissions.ts b/packages/agents/src/permissions.ts new file mode 100644 index 0000000..bf087f1 --- /dev/null +++ b/packages/agents/src/permissions.ts @@ -0,0 +1,57 @@ +/** + * Tool Permissions System + * + * Runtime access control for tool execution + */ + +import { ToolPermissionDeniedError } from "./guardrails.js"; +import type { ToolPermissionContext, ToolPermissions } from "./types.js"; + +/** + * Check if a tool can be executed based on permissions + */ +export async function checkToolPermission( + permissions: ToolPermissions | undefined, + toolName: string, + args: unknown, + context: ToolPermissionContext, +): Promise { + if (!permissions) return; + + try { + const result = await permissions.check({ toolName, args, context }); + + if (!result.allowed) { + throw new ToolPermissionDeniedError( + toolName, + result.reason || "Permission denied", + ); + } + } catch (error) { + if (error instanceof ToolPermissionDeniedError) { + throw error; + } + // Re-throw other errors as-is + throw error; + } +} + +/** + * Create a tool usage tracker for permission context + */ +export function createUsageTracker(): ToolPermissionContext["usage"] { + return { + toolCalls: {}, + tokens: 0, + }; +} + +/** + * Update usage tracker with tool call + */ +export function trackToolCall( + usage: ToolPermissionContext["usage"], + toolName: string, +): void { + usage.toolCalls[toolName] = (usage.toolCalls[toolName] || 0) + 1; +} diff --git a/packages/agents/src/routing.ts b/packages/agents/src/routing.ts new file mode 100644 index 0000000..14562c0 --- /dev/null +++ b/packages/agents/src/routing.ts @@ -0,0 +1,106 @@ +/** + * Programmatic Routing System + * + * Matches user messages to agents based on keywords, patterns, or custom functions + */ + +import type { Agent } from "./types.js"; + +/** + * Normalize text for better matching + * - Lowercase + * - Remove numbers + * - Remove extra whitespace + * - Simple plural โ†’ singular + */ +function normalizeText(text: string): string { + return text + .toLowerCase() + .trim() + .replace(/\d+/g, "") // remove numbers + .replace(/\s+/g, " ") // normalize whitespace + .trim(); +} + +/** + * Match a message against an agent's matchOn patterns + */ +export function matchAgent( + agent: Agent, + message: string, + matchOn?: (string | RegExp)[] | ((message: string) => boolean), +): { matched: boolean; score: number } { + if (!matchOn) { + return { matched: false, score: 0 }; + } + + const normalizedMessage = normalizeText(message); + let score = 0; + + // Function-based matching + if (typeof matchOn === "function") { + try { + const result = matchOn(message); + return { matched: result, score: result ? 10 : 0 }; + } catch (error) { + console.error( + `[Routing] Error in matchOn function for ${agent.name}:`, + error, + ); + return { matched: false, score: 0 }; + } + } + + // Array-based matching (strings and regex) + for (const pattern of matchOn) { + if (typeof pattern === "string") { + // String keyword matching + const normalizedPattern = normalizeText(pattern); + if (normalizedMessage.includes(normalizedPattern)) { + // Weight longer keywords higher (more specific) + const weight = normalizedPattern.split(" ").length; + score += weight; + } + } else if (pattern instanceof RegExp) { + // Regex pattern matching + if (pattern.test(normalizedMessage)) { + score += 2; // Regex matches get higher weight + } + } + } + + return { matched: score > 0, score }; +} + +/** + * Find the best matching agent from a list of agents + */ +export function findBestMatch( + agents: Agent[], + message: string, + getMatchOn?: ( + agent: Agent, + ) => (string | RegExp)[] | ((message: string) => boolean) | undefined, +): Agent | null { + const scores: Array<{ agent: Agent; score: number }> = []; + + for (const agent of agents) { + const matchOn = getMatchOn ? getMatchOn(agent) : undefined; + const { matched, score } = matchAgent(agent, message, matchOn); + + if (matched && score > 0) { + scores.push({ agent, score }); + } + } + + // No matches found + if (scores.length === 0) { + return null; + } + + // Sort by score (highest first) + scores.sort((a, b) => b.score - a.score); + + // Return agent with highest score + return scores[0].agent; +} diff --git a/packages/agents/src/runner.ts b/packages/agents/src/runner.ts deleted file mode 100644 index 3b44618..0000000 --- a/packages/agents/src/runner.ts +++ /dev/null @@ -1,517 +0,0 @@ -import type { ModelMessage } from "ai"; -import { debug } from "./debug.js"; -import { isHandoffResult } from "./handoff.js"; -import type { - Agent, - AgentGenerateResult, - AgentStreamingResult, - HandoffInstruction, - RunOptions, - StreamChunk, -} from "./types.js"; - -/** - * Multi-agent execution engine that handles orchestration and handoffs - */ -export class Runner { - private agents: Map = new Map(); - private options: RunOptions; - - constructor(options: RunOptions = {}) { - this.options = { - maxTotalTurns: 20, - ...options, - }; - } - - /** - * Register an agent with the runner - */ - registerAgent(agent: Agent): void { - this.agents.set(agent.name, agent); - } - - /** - * Register multiple agents - */ - registerAgents(agents: Agent[]): void { - for (const agent of agents) { - this.registerAgent(agent); - } - } - - /** - * Run a multi-agent conversation starting with the specified agent - */ - async run( - startingAgent: Agent | string, - input: string, - options?: Partial, - ): Promise { - const runOptions = { ...this.options, ...options }; - const maxTotalTurns = runOptions.maxTotalTurns || 20; - - // Get starting agent - const initialAgent = - typeof startingAgent === "string" - ? this.agents.get(startingAgent) - : startingAgent; - - if (!initialAgent) { - throw new Error( - `Agent not found: ${typeof startingAgent === "string" ? startingAgent : "unknown"}`, - ); - } - - // Register all agents that might be needed - this.registerAgent(initialAgent); - this.registerHandoffAgents(initialAgent); - - let currentAgent = initialAgent; - let totalTurns = 0; - const messages: ModelMessage[] = []; - - while (totalTurns < maxTotalTurns) { - try { - // Run current agent - const result = await currentAgent.generate({ - prompt: input, - messages, - }); - - totalTurns += result.steps?.length || 1; - - // Update messages from steps - if (result.steps) { - for (const step of result.steps) { - if (step.text) { - messages.push({ - role: "assistant", - content: step.text, - }); - } - } - } - - // Check for handoffs - let handoffFound = false; - if (result.steps) { - for (const step of result.steps) { - if (step.toolCalls) { - for (const toolCall of step.toolCalls) { - // Find the corresponding tool result - const toolResult = step.toolResults?.find( - (tr) => tr.toolCallId === toolCall.toolCallId, - ); - if (toolResult && isHandoffResult(toolResult.output)) { - const handoff = toolResult.output as HandoffInstruction; - - // Notify callback - if (runOptions.onHandoff) { - runOptions.onHandoff(handoff); - } - - // Find target agent - const targetAgent = this.agents.get(handoff.targetAgent); - if (!targetAgent) { - throw new Error( - `Target agent not found: ${handoff.targetAgent}`, - ); - } - - // Add handoff context to messages if provided - if (handoff.context) { - messages.push({ - role: "system", - content: `Handoff context: ${handoff.context}${handoff.reason ? ` (Reason: ${handoff.reason})` : ""}`, - }); - } - - // Switch to target agent - currentAgent = targetAgent; - handoffFound = true; - - // Clear input for subsequent agents (they'll use messages) - input = ""; - break; - } - } - } - if (handoffFound) break; - } - } - - // If no handoff, we're done - if (!handoffFound) { - // Return the result with finalOutput properly set - return { - ...result, - finalOutput: result.finalOutput || result.text || "", - }; - } - } catch (error) { - throw new Error( - `Error during execution: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } - } - - throw new Error("Maximum turns exceeded"); - } - - private registerHandoffAgents(agent: Agent): void { - const handoffs = agent.getHandoffs(); - for (const handoffAgent of handoffs) { - this.registerAgent(handoffAgent); - // Recursively register their handoff agents too - this.registerHandoffAgents(handoffAgent); - } - } - - getAgents(): Agent[] { - return Array.from(this.agents.values()); - } - - getAgent(name: string): Agent | undefined { - return this.agents.get(name); - } - - /** - * Run a streaming multi-agent conversation - */ - async runStream( - startingAgent: Agent | string, - input: string, - options?: Partial, - ): Promise { - const runOptions = { ...this.options, ...options }; - const maxTotalTurns = runOptions.maxTotalTurns || 20; - - // Get starting agent - const initialAgent = - typeof startingAgent === "string" - ? this.agents.get(startingAgent) - : startingAgent; - - if (!initialAgent) { - throw new Error( - `Agent not found: ${typeof startingAgent === "string" ? startingAgent : "unknown"}`, - ); - } - - this.registerAgent(initialAgent); - this.registerHandoffAgents(initialAgent); - - let currentAgent = initialAgent; - let totalTurns = 0; - // Only use initialMessages if explicitly provided (not empty array) - const messages: ModelMessage[] = runOptions.initialMessages || []; - let resolvedResult: AgentGenerateResult | null = null; - const originalInput = input; - const handoffHistory: HandoffInstruction[] = []; - const startTime = new Date(); - - // Create streaming generator that yields structured data immediately - const streamGenerator = async function* ( - this: Runner, - ): AsyncGenerator { - // Start orchestration - debug("ORCHESTRATION", `Starting with ${currentAgent.name}`, { - handoffAgents: currentAgent.getHandoffs().map((a) => a.name), - maxTurns: maxTotalTurns, - }); - - // Immediate feedback - orchestration starting - yield { - type: "orchestration-status", - status: "planning", - agent: currentAgent.name, - role: "system", - }; - - while (totalTurns < maxTotalTurns) { - try { - debug( - "ORCHESTRATION", - `Turn ${totalTurns + 1}: ${currentAgent.name} executing`, - ); - - // Show agent thinking - yield { - type: "agent-thinking", - agent: currentAgent.name, - task: input, - role: "system", - }; - - yield { - type: "orchestration-status", - status: "executing", - agent: currentAgent.name, - role: "system", - }; - - // Ensure we have a valid prompt - // If we have messages (after handoff), use a simple continuation prompt - const promptToUse = - messages.length > 0 - ? input || "Please proceed with the task." - : input || originalInput || "Continue the conversation"; - - // Only pass messages if we have any (don't pass empty array) - const stream = await currentAgent.stream( - messages.length > 0 - ? { prompt: promptToUse, messages } - : { prompt: promptToUse, messages: undefined }, - ); - - debug( - "STREAM", - `Starting to consume textStream for ${currentAgent.name}`, - ); - let chunkCount = 0; - const textChunks: string[] = []; - - // Always collect chunks, and stream them immediately - for await (const chunk of stream.textStream) { - chunkCount++; - textChunks.push(chunk); - debug( - "STREAM", - `Chunk ${chunkCount} from ${currentAgent.name}:`, - chunk.substring(0, 50), - ); - - // Always yield immediately - we'll suppress orchestrator text in agent.ts - yield { - type: "text-delta", - text: chunk, - agent: currentAgent.name, - role: "assistant", - }; - } - debug( - "STREAM", - `textStream ended for ${currentAgent.name}, total chunks: ${chunkCount}`, - ); - - // Get the completed result for handoff detection - const result = await stream; - - const steps = await result.steps; - totalTurns += steps?.length || 1; - - // Check for handoffs - let handoffFound = false; - if (steps) { - for (const step of steps) { - if (step.toolCalls) { - debug( - "ORCHESTRATION", - `Found ${step.toolCalls.length} tool calls`, - { - tools: step.toolCalls.map((tc) => tc.toolName), - }, - ); - for (const toolCall of step.toolCalls) { - yield { - type: "tool-call", - toolName: toolCall.toolName, - args: - (toolCall as { input?: Record }).input || - {}, - agent: currentAgent.name, - role: "assistant", - }; - } - } - - if (step.toolResults) { - for (const toolResult of step.toolResults) { - yield { - type: "tool-result", - toolName: toolResult.toolName, - result: toolResult.output, - agent: currentAgent.name, - role: "assistant", - }; - - if (isHandoffResult(toolResult.output)) { - const handoff = toolResult.output as HandoffInstruction; - console.log("DEBUG: Handoff detected:", handoff); - handoffHistory.push(handoff); // Track handoffs - handoffFound = true; // Mark that we found a handoff - - yield { - type: "agent-switch", - fromAgent: currentAgent.name, - toAgent: handoff.targetAgent, - reason: handoff.reason, - context: handoff.context, - agent: currentAgent.name, - role: "system", - }; - - // Show progress update - yield { - type: "orchestration-status", - status: "routing", - agent: handoff.targetAgent, - role: "system", - }; - - // Find target agent - const targetAgent = this.agents.get(handoff.targetAgent); - if (!targetAgent) { - yield { - type: "error", - error: `Target agent not found: ${handoff.targetAgent}`, - agent: currentAgent.name, - role: "system", - }; - return; - } - - // Add handoff context with original question - if (handoff.context) { - messages.push({ - role: "system", - content: `Handoff context: ${handoff.context}${handoff.reason ? ` (Reason: ${handoff.reason})` : ""}. Original request: ${originalInput}`, - }); - } else { - messages.push({ - role: "system", - content: `You are being handed off to handle: ${originalInput}`, - }); - } - - currentAgent = targetAgent; - handoffFound = true; - input = ""; // Clear input, let agent respond to system message context - console.log("DEBUG: Switched to agent:", currentAgent.name); - break; - } - } - } - if (handoffFound) break; - } - } - - // If no handoff, we're done - if (!handoffFound) { - const finalText = (await result.text) || ""; - yield { - type: "agent-complete", - agent: currentAgent.name, - finalOutput: finalText || "", - role: "assistant", - }; - - const endTime = new Date(); - resolvedResult = { - text: finalText || "", - finalAgent: currentAgent.name, - finalOutput: finalText || "", - handoffs: handoffHistory, // Use tracked handoffs - metadata: { - startTime, - endTime, - duration: endTime.getTime() - startTime.getTime(), // Proper duration - }, - steps: await result.steps, - finishReason: await result.finishReason, - usage: await result.usage, - }; - break; - } - } catch (error) { - yield { - type: "error", - error: error instanceof Error ? error.message : "Unknown error", - agent: currentAgent.name, - role: "system", - }; - break; - } - } - }.bind(this); - - const streamInstance = streamGenerator(); - - return { - stream: streamInstance, - result: (async () => { - // Wait for the stream to complete and return the resolved result - // Don't consume the stream here as it's being consumed elsewhere - let attempts = 0; - const maxAttempts = 100; // Wait up to 10 seconds - - while (!resolvedResult && attempts < maxAttempts) { - await new Promise((resolve) => setTimeout(resolve, 100)); - attempts++; - } - - if (resolvedResult) { - return resolvedResult; - } else { - // Create a basic result if none was set - const endTime = new Date(); - return { - text: "", - finalAgent: currentAgent.name, - finalOutput: "", - handoffs: handoffHistory, - metadata: { - startTime, - endTime, - duration: endTime.getTime() - startTime.getTime(), - }, - steps: [], - finishReason: "stop", - usage: { totalTokens: 0, promptTokens: 0, completionTokens: 0 }, - }; - } - })(), - }; - } -} - -export async function run( - agent: Agent | string, - input: string, - options: RunOptions & { stream: true }, -): Promise; - -export async function run( - agent: Agent | string, - input: string, - options?: RunOptions & { stream?: false }, -): Promise; - -export async function run( - agent: Agent | string, - input: string, - options?: RunOptions & { stream?: boolean }, -): Promise { - const runner = new Runner(options); - - if (options?.stream) { - // Use the working streaming implementation - return runner.runStream(agent, input, options); - } - - return runner.run(agent, input, options); -} - -/** - * Legacy streaming function (use run with { stream: true } instead) - * @deprecated Use run(agent, input, { stream: true }) instead - */ -export async function runStream( - agent: Agent | string, - input: string, - options?: RunOptions, -): Promise { - const runner = new Runner(options); - return runner.runStream(agent, input, options); -} diff --git a/packages/agents/src/streaming.ts b/packages/agents/src/streaming.ts new file mode 100644 index 0000000..9435858 --- /dev/null +++ b/packages/agents/src/streaming.ts @@ -0,0 +1,91 @@ +/** + * Type-safe streaming utilities for agent orchestration + * + * This module provides helper functions for writing custom data parts + * to the UI message stream following the AI SDK's streaming data pattern. + */ + +import type { UIMessageStreamWriter } from "ai"; +import type { AgentDataParts } from "./types.js"; + +/** + * Write a typed data part to the stream. + * + * This helper provides type-safe access to agent data parts while handling + * the necessary type assertions for AI SDK's internal types. + * + * @template K - Key of the data part type + * @param writer - The UI message stream writer + * @param type - The data part type (e.g., 'data-agent-status') + * @param data - The data payload + * @param options - Additional options (transient, id for reconciliation) + * + * @example + * ```typescript + * writeDataPart(writer, 'data-agent-status', { + * status: 'executing', + * agent: 'analytics' + * }, { transient: true }); + * ``` + */ +export function writeDataPart( + writer: UIMessageStreamWriter, + type: `data-${K}`, + data: AgentDataParts[K], + options?: { transient?: boolean; id?: string }, +): void { + writer.write({ + type, + data, + ...options, + } as never); +} + +/** + * Write a transient agent status update. + * + * Status updates are ephemeral and won't be added to message history. + * They're only available via the onData callback in useChat. + * + * @param writer - The UI message stream writer + * @param status - The status update data + * + * @example + * ```typescript + * writeAgentStatus(writer, { + * status: 'routing', + * agent: 'orchestrator' + * }); + * ``` + */ +export function writeAgentStatus( + writer: UIMessageStreamWriter, + status: AgentDataParts["agent-status"], +): void { + writeDataPart(writer, "data-agent-status", status, { transient: true }); +} + +/** + * Write a transient rate limit update. + * + * Rate limit updates are ephemeral and won't be added to message history. + * They're only available via the onData callback in useChat. + * + * @param writer - The UI message stream writer + * @param rateLimit - The rate limit data + * + * @example + * ```typescript + * writeRateLimit(writer, { + * limit: 100, + * remaining: 95, + * reset: '2024-01-01T00:00:00Z' + * }); + * ``` + */ +export function writeRateLimit( + writer: UIMessageStreamWriter, + rateLimit: AgentDataParts["rate-limit"], +): void { + writeDataPart(writer, "data-rate-limit", rateLimit, { transient: true }); +} diff --git a/packages/agents/src/types.ts b/packages/agents/src/types.ts index cf4ba82..f43fe39 100644 --- a/packages/agents/src/types.ts +++ b/packages/agents/src/types.ts @@ -1,37 +1,64 @@ import type { + IdGenerator, LanguageModel, + LanguageModelUsage, ModelMessage, StepResult, StreamTextResult, Tool, + UIMessage, + UIMessageStreamOnFinishCallback, + UIMessageStreamWriter, } from "ai"; // Forward declaration -export interface Agent { +export interface Agent< + TContext extends Record = Record, +> { name: string; - instructions: string; + instructions: string | ((context: TContext) => string); + matchOn?: (string | RegExp)[] | ((message: string) => boolean); + onEvent?: (event: AgentEvent) => void | Promise; + inputGuardrails?: InputGuardrail[]; + outputGuardrails?: OutputGuardrail[]; + permissions?: ToolPermissions; generate(options: AgentGenerateOptions): Promise; - stream(options: AgentStreamOptions): Promise; - getHandoffs(): Agent[]; + stream(options: AgentStreamOptions): AgentStreamResult; + getHandoffs(): Array>; } -export interface AgentConfig { +export interface AgentConfig< + TContext extends Record = Record, +> { /** Unique name for the agent */ name: string; - /** System instructions for the agent */ - instructions: string; + /** + * Static instructions or dynamic function that receives context. + * Function receives the full execution context and returns the system prompt. + */ + instructions: string | ((context: TContext) => string); /** Language model to use */ model: LanguageModel; /** Tools available to the agent */ tools?: Record; /** Agents this agent can hand off to */ - handoffs?: Agent[]; + handoffs?: Array>; /** Maximum number of turns before stopping */ maxTurns?: number; /** Temperature for model responses */ temperature?: number; /** Additional model settings */ modelSettings?: Record; + /** Programmatic routing patterns */ + matchOn?: (string | RegExp)[] | ((message: string) => boolean); + /** Lifecycle event handler */ + onEvent?: (event: AgentEvent) => void | Promise; + /** Input guardrails - run before agent execution */ + inputGuardrails?: InputGuardrail[]; + /** Output guardrails - run after agent execution */ + outputGuardrails?: OutputGuardrail[]; + /** Tool permissions - control tool access */ + permissions?: ToolPermissions; } export interface HandoffInstruction { @@ -55,7 +82,7 @@ export interface AgentGenerateOptions { * Stream options for agents */ export interface AgentStreamOptions { - prompt: string; + prompt?: string; messages?: ModelMessage[]; } @@ -67,8 +94,12 @@ export interface AgentGenerateResult { metadata: { startTime: Date; endTime: Date; duration: number }; steps?: StepResult>[]; finishReason?: string; - usage?: unknown; - toolCalls?: unknown[]; + usage?: LanguageModelUsage; + toolCalls?: Array<{ + toolCallId: string; + toolName: string; + args: unknown; + }>; } /** @@ -77,93 +108,217 @@ export interface AgentGenerateResult { export type AgentStreamResult = StreamTextResult, never>; /** - * Run options for multi-agent orchestration + * Lifecycle events emitted by agents + */ +export type AgentEvent = + | { type: "start"; agent: string; input: string } + | { type: "agent-start"; agent: string; round: number } + | { + type: "agent-step"; + agent: string; + step: StepResult>; + } + | { type: "agent-finish"; agent: string; round: number } + | { type: "agent-handoff"; from: string; to: string; reason?: string } + | { type: "agent-complete"; totalRounds: number } + | { type: "agent-error"; error: Error } + | { type: "tool-call"; agent: string; toolName: string; args: unknown } + | { type: "handoff"; from: string; to: string; reason?: string } + | { type: "complete"; agent: string; output: string } + | { type: "error"; agent: string; error: Error }; + +/** + * Guardrail result */ -export interface RunOptions { - /** Maximum total turns across all agents */ - maxTotalTurns?: number; - /** Callback for handoff events */ - onHandoff?: (handoff: HandoffInstruction) => void; - /** Initial message history to start with */ - initialMessages?: ModelMessage[]; +export interface GuardrailResult { + tripwireTriggered: boolean; + outputInfo?: unknown; } /** - * Base chunk with common properties + * Input guardrail - runs before agent execution */ -interface BaseStreamChunk { - agent: string; - timestamp?: Date; +export interface InputGuardrail { + name: string; + execute: (args: { + input: string; + context?: unknown; + }) => Promise; } /** - * Role-based streaming chunks following AI SDK patterns + * Output guardrail - runs after agent execution */ -export type StreamChunk = - | (BaseStreamChunk & { - type: "text-delta"; - text: string; - role: "assistant"; - }) - | (BaseStreamChunk & { - type: "agent-switch"; - fromAgent: string; - toAgent: string; - reason?: string; - context?: string; - role: "system"; - }) - | (BaseStreamChunk & { - type: "tool-call"; - toolName: string; - args: Record; - role: "assistant"; - }) - | (BaseStreamChunk & { - type: "tool-result"; - toolName: string; - result: unknown; - role: "assistant"; - }) - | (BaseStreamChunk & { - type: "agent-complete"; - finalOutput: string; - role: "assistant"; - }) - | (BaseStreamChunk & { - type: "error"; - error: string; - role: "system"; - }) - | (BaseStreamChunk & { - type: "orchestration-status"; - status: "planning" | "routing" | "executing" | "completed"; - role: "system"; - }) - | (BaseStreamChunk & { - type: "agent-thinking"; - task: string; - role: "system"; - }) - | (BaseStreamChunk & { - type: "workflow-progress"; - currentStep: number; - totalSteps: number; - stepName: string; - role: "system"; - }); +export interface OutputGuardrail { + name: string; + execute: (args: { + agentOutput: TOutput; + context?: unknown; + }) => Promise; +} /** - * Streaming result for multi-agent workflows + * Tool permission context */ -export interface AgentStreamingResult { - /** Async iterator for streaming chunks */ - stream: AsyncIterable; - /** Final result (available after stream completes) */ - result: Promise; +export interface ToolPermissionContext { + user?: { id: string; roles: string[]; [key: string]: unknown }; + usage: { toolCalls: Record; tokens: number }; + [key: string]: unknown; } -export interface AgentRunOptions { - metadata?: any; - maxTotalTurns?: number; +/** + * Tool permission result + */ +export interface ToolPermissionResult { + allowed: boolean; + reason?: string; } + +/** + * Tool permission check function + */ +export type ToolPermissionCheck = (ctx: { + toolName: string; + args: unknown; + context: ToolPermissionContext; +}) => ToolPermissionResult | Promise; + +/** + * Tool permissions configuration + */ +export interface ToolPermissions { + check: ToolPermissionCheck; +} + +/** + * Options for agent.toUIMessageStream() + */ +export interface AgentStreamOptionsUI< + TContext extends Record = Record, +> { + // Agent-specific options + /** Message history - last message is used as input for routing */ + messages: ModelMessage[]; + /** Routing strategy */ + strategy?: "auto" | "llm"; + /** Max orchestration rounds */ + maxRounds?: number; + /** Max steps per agent */ + maxSteps?: number; + /** Global timeout (ms) */ + timeout?: number; + /** + * Context for permissions, guardrails, and artifacts. + * This object will be wrapped in RunContext and passed to all tools and hooks. + * The writer will be automatically added when streaming. + */ + context?: TContext; + /** Hook before streaming starts */ + beforeStream?: (ctx: { + writer: UIMessageStreamWriter; + }) => Promise; + /** Lifecycle event handler */ + onEvent?: (event: AgentEvent) => void | Promise; + + // AI SDK createUIMessageStream options + /** Callback when stream finishes with final messages */ + onFinish?: UIMessageStreamOnFinishCallback; + /** Process errors, e.g. to log them. Returns error message for data stream */ + onError?: (error: unknown) => string; + /** Generate message ID for the response message */ + generateId?: IdGenerator; + + // AI SDK toUIMessageStream options + /** Send reasoning parts to client (default: true) */ + sendReasoning?: boolean; + /** Send source parts to client (default: false) */ + sendSources?: boolean; + /** Send finish event to client (default: true) */ + sendFinish?: boolean; + /** Send message start event to client (default: true) */ + sendStart?: boolean; + /** Extract message metadata to send to client */ + messageMetadata?: (options: { + part: unknown; + }) => Record | undefined; + + // AI SDK response options + /** AI SDK transform - stream transform function */ + experimental_transform?: unknown; + /** HTTP status code */ + status?: number; + /** HTTP status text */ + statusText?: string; + /** HTTP headers */ + headers?: Record; +} + +/** + * Base data part schemas for agent orchestration streaming. + * Users can extend this interface to add custom data parts. + * + * @example Extending with custom data parts + * ```typescript + * declare module '@ai-sdk-tools/agents' { + * interface AgentDataParts { + * 'custom-data': { + * value: string; + * timestamp: number; + * }; + * } + * } + * ``` + */ +export interface AgentDataParts { + /** Agent status updates (transient - won't be in message history) */ + "agent-status": { + status: "routing" | "executing" | "completing"; + agent: string; + }; + /** Rate limit information (transient) */ + "rate-limit": { + limit: number; + remaining: number; + reset: string; + code?: string; + }; + // Allow extension with custom data parts + [key: string]: unknown; +} + +/** + * Generic UI Message type for agents with orchestration data parts. + * Extends AI SDK's UIMessage with agent-specific data parts. + * + * @template TMetadata - Message metadata type (default: never) + * @template TDataParts - Custom data parts type (default: AgentDataParts) + * + * @example Basic usage + * ```typescript + * import type { AgentUIMessage } from '@ai-sdk-tools/agents'; + * + * const { messages } = useChat({ + * api: '/api/chat', + * onData: (dataPart) => { + * if (dataPart.type === 'data-agent-status') { + * console.log('Agent status:', dataPart.data); + * } + * } + * }); + * ``` + * + * @example With custom data parts + * ```typescript + * interface MyDataParts extends AgentDataParts { + * 'custom-metric': { value: number }; + * } + * + * const { messages } = useChat>({ + * api: '/api/chat' + * }); + * ``` + */ +export type AgentUIMessage< + TMetadata = never, + TDataParts extends Record = AgentDataParts, +> = UIMessage; diff --git a/packages/agents/src/utils.ts b/packages/agents/src/utils.ts new file mode 100644 index 0000000..f772b7a --- /dev/null +++ b/packages/agents/src/utils.ts @@ -0,0 +1,38 @@ +import type { ModelMessage } from "ai"; + +/** + * Extract text content from a ModelMessage. + * Handles both string content and content arrays with text parts. + * + * @param message - The message to extract text from + * @returns The extracted text content, or an empty string if none found + * + * @example + * ```ts + * const text = extractTextFromMessage(message); + * // "Hello world" + * ``` + */ +export function extractTextFromMessage( + message: ModelMessage | undefined, +): string { + if (!message?.content) return ""; + + const { content } = message; + + // String content + if (typeof content === "string") return content; + + // Array of parts - extract all text parts and join them + if (Array.isArray(content)) { + return content + .filter( + (part): part is { type: "text"; text: string } => + typeof part === "object" && part !== null && part.type === "text", + ) + .map((part) => part.text) + .join(""); + } + + return ""; +} diff --git a/packages/artifacts/README.md b/packages/artifacts/README.md index 47bdaa8..7caeda8 100644 --- a/packages/artifacts/README.md +++ b/packages/artifacts/README.md @@ -2,15 +2,15 @@ Advanced streaming interfaces for AI applications. Create structured, type-safe artifacts that stream real-time updates from AI tools to React components. -## โœจ Features +## Features -- ๐ŸŽฏ **Type-Safe Streaming** - Full TypeScript support with Zod schema validation -- ๐Ÿ”„ **Real-time Updates** - Stream partial updates with progress tracking -- ๐ŸŽจ **Clean API** - Minimal boilerplate, maximum flexibility -- ๐Ÿช **State Management** - Built on @ai-sdk-tools/store for efficient message handling -- โšก **Performance Optimized** - Efficient state management and updates +- **Type-Safe Streaming** - Full TypeScript support with Zod schema validation +- **Real-time Updates** - Stream partial updates with progress tracking +- **Clean API** - Minimal boilerplate, maximum flexibility +- **State Management** - Built on @ai-sdk-tools/store for efficient message handling +- **Performance Optimized** - Efficient state management and updates -## ๐Ÿ“ฆ Installation +## Installation ```bash npm install @ai-sdk-tools/artifacts @ai-sdk-tools/store @@ -23,7 +23,7 @@ npm install @ai-sdk-tools/artifacts @ai-sdk-tools/store The artifacts package uses the store package's `useChatMessages` hook to efficiently extract and track artifact data from AI SDK message streams, ensuring optimal performance and avoiding unnecessary re-renders. -## ๐Ÿ”ง Setup +## Setup ### 1. Initialize Chat with Store @@ -52,7 +52,7 @@ function ChatComponent() { The `useArtifact` hook automatically connects to the global chat store to extract artifact data from message streams - no prop drilling needed! -## ๐Ÿš€ Quick Start +## Quick Start ### 1. Define an Artifact @@ -187,7 +187,7 @@ function Analysis() { } ``` -## ๐Ÿ“š API Reference +## API Reference ### `artifact(id, schema)` Creates an artifact definition with Zod schema validation. @@ -267,7 +267,7 @@ function Canvas() { -## ๐Ÿ”ง Advanced Usage +## Advanced Usage ### Combining Both Hooks @@ -332,7 +332,7 @@ function DashboardWithAnalysis() { - You want to show an overview of all available artifacts - You're building a generic artifact renderer -## ๐Ÿ”ง Examples +## Examples See the `src/examples/` directory for complete examples including: - Burn rate analysis with progress tracking @@ -340,6 +340,10 @@ See the `src/examples/` directory for complete examples including: - Route setup and tool implementation - Using `useArtifacts` for multi-type artifact rendering -## ๐Ÿ“„ License +## Contributing + +Contributions are welcome! See the [contributing guide](../../CONTRIBUTING.md) for details. + +## License MIT diff --git a/packages/artifacts/src/artifact.ts b/packages/artifacts/src/artifact.ts index 976f978..3748060 100644 --- a/packages/artifacts/src/artifact.ts +++ b/packages/artifacts/src/artifact.ts @@ -1,3 +1,4 @@ +import type { UIMessageStreamWriter } from "ai"; import type { z } from "zod"; import { StreamingArtifact } from "./streaming"; import type { ArtifactConfig, ArtifactData } from "./types"; @@ -25,10 +26,13 @@ export function artifact(id: string, schema: z.ZodSchema) { }; }, - stream(data: Partial = {}): StreamingArtifact { + stream( + data: Partial, + writer: UIMessageStreamWriter, + ): StreamingArtifact { const instance = this.create(data); instance.status = "loading"; - return new StreamingArtifact(config, instance); + return new StreamingArtifact(config, instance, writer); }, validate(data: unknown): T { diff --git a/packages/artifacts/src/client.ts b/packages/artifacts/src/client.ts index 7621188..66ed639 100644 --- a/packages/artifacts/src/client.ts +++ b/packages/artifacts/src/client.ts @@ -2,10 +2,7 @@ // Re-export all other exports from main index export { artifact } from "./artifact"; -export { - artifacts, - createTypedContext, -} from "./context"; +export { getWriter } from "./context"; export { useArtifact, useArtifacts } from "./hooks"; export { StreamingArtifact } from "./streaming"; @@ -15,7 +12,6 @@ export type { ArtifactConfig, ArtifactData, ArtifactStatus, - BaseContext, UseArtifactReturn, UseArtifactsOptions, UseArtifactsReturn, diff --git a/packages/artifacts/src/context.ts b/packages/artifacts/src/context.ts index 38e960f..5726679 100644 --- a/packages/artifacts/src/context.ts +++ b/packages/artifacts/src/context.ts @@ -1,69 +1,38 @@ -import type { UIMessageStreamWriter } from "ai"; -import type { BaseContext } from "./types"; -import { ArtifactError } from "./types"; - -class ArtifactSystem { - private context: TContext | null = null; - - setContext(context: T): void { - this.context = context as unknown as TContext; - } - - getWriter(): UIMessageStreamWriter { - if (!this.context?.writer) { - throw new ArtifactError( - "WRITER_NOT_AVAILABLE", - "No artifact writer available. Make sure to set context with writer in your route handler before using artifacts.", - ); - } - return this.context.writer; - } +/** + * Artifacts Writer Access + * + * Provides access to the stream writer from tool executionOptions. + * The writer is needed to stream artifact updates to the client. + * + */ - getContext(): TContext { - if (!this.context) { - throw new ArtifactError( - "CONTEXT_NOT_SET", - "Artifact context not available. Make sure to call setContext() in your route handler before using artifacts.", - ); - } - return this.context; - } - - clearContext(): void { - this.context = null; - } +import type { UIMessageStreamWriter } from "ai"; - isActive(): boolean { - return this.context !== null; +/** + * Get writer from execution context + * + * @param executionOptions - Tool execution options from AI SDK + * @returns The stream writer + * + * @example + * ```typescript + * export const myTool = tool({ + * execute: async (params, executionOptions) => { + * const writer = getWriter(executionOptions); + * const artifact = MyArtifact.stream(data, writer); + * } + * }); + * ``` + */ +export function getWriter(executionOptions?: any): UIMessageStreamWriter { + // AI SDK passes context via experimental_context + const writer = executionOptions?.experimental_context?.writer; + + if (!writer) { + throw new Error( + "Writer not available. Make sure you're passing executionOptions: getWriter(executionOptions)", + ); } -} - -// Global artifact system instance -export const artifacts = new ArtifactSystem(); -// Typed context helper factory -export function createTypedContext( - validator?: (context: T) => boolean | string, -) { - return { - setContext: (context: T) => { - if (validator) { - const result = validator(context); - if (result === false) { - throw new ArtifactError( - "CONTEXT_VALIDATION_FAILED", - "Context validation failed", - ); - } - if (typeof result === "string") { - throw new ArtifactError("CONTEXT_VALIDATION_FAILED", result); - } - } - artifacts.setContext(context); - }, - getContext: (): T => artifacts.getContext() as T, - getWriter: () => artifacts.getWriter(), - clearContext: () => artifacts.clearContext(), - isActive: () => artifacts.isActive(), - }; + return writer; } diff --git a/packages/artifacts/src/index.ts b/packages/artifacts/src/index.ts index 01c45aa..87eb8c5 100644 --- a/packages/artifacts/src/index.ts +++ b/packages/artifacts/src/index.ts @@ -1,9 +1,6 @@ // Core exports (server-safe - no React hooks) export { artifact } from "./artifact"; -export { - artifacts, - createTypedContext, -} from "./context"; +export { getWriter } from "./context"; export { StreamingArtifact } from "./streaming"; // Type exports @@ -12,7 +9,6 @@ export type { ArtifactConfig, ArtifactData, ArtifactStatus, - BaseContext, UseArtifactReturn, UseArtifactsOptions, UseArtifactsReturn, diff --git a/packages/artifacts/src/streaming.ts b/packages/artifacts/src/streaming.ts index f3ee085..c923c89 100644 --- a/packages/artifacts/src/streaming.ts +++ b/packages/artifacts/src/streaming.ts @@ -1,13 +1,19 @@ -import { artifacts } from "./context"; +import type { UIMessageStreamWriter } from "ai"; import type { ArtifactConfig, ArtifactData } from "./types"; export class StreamingArtifact { private config: ArtifactConfig; private instance: ArtifactData; + private writer: UIMessageStreamWriter; - constructor(config: ArtifactConfig, instance: ArtifactData) { + constructor( + config: ArtifactConfig, + instance: ArtifactData, + writer: UIMessageStreamWriter, + ) { this.config = config; this.instance = instance; + this.writer = writer; // Send initial state this.stream(); @@ -83,8 +89,7 @@ export class StreamingArtifact { } private stream(): void { - const writer = artifacts.getWriter(); - writer.write({ + this.writer.write({ type: `data-artifact-${this.config.id}`, id: this.instance.id, data: this.instance, diff --git a/packages/artifacts/src/types.ts b/packages/artifacts/src/types.ts index 503b8b0..1bf0959 100644 --- a/packages/artifacts/src/types.ts +++ b/packages/artifacts/src/types.ts @@ -1,4 +1,3 @@ -import type { UIMessageStreamWriter } from "ai"; import type { z } from "zod"; export type ArtifactStatus = @@ -8,10 +7,6 @@ export type ArtifactStatus = | "complete" | "error"; -export interface BaseContext { - writer: UIMessageStreamWriter; -} - export class ArtifactError extends Error { constructor( public code: string, diff --git a/packages/cache/README.md b/packages/cache/README.md index 09faa39..6f79af2 100644 --- a/packages/cache/README.md +++ b/packages/cache/README.md @@ -4,7 +4,7 @@ Universal caching wrapper for AI SDK tools. Cache expensive tool executions with zero configuration - works with regular tools, streaming tools, and artifacts. -## โšก Why Cache Tools? +## Why Cache Tools? AI agents repeatedly call expensive tools: - **Same API calls** across conversation turns (weather, translations) diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 3168b35..d5fde19 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -1,20 +1,18 @@ import type { Tool } from "ai"; -import type { CacheOptions, CachedTool, CacheStats, CacheStore } from "./types"; -import { LRUCacheStore } from "./cache-store"; import { createCacheBackend } from "./backends/factory"; - - +import { LRUCacheStore } from "./cache-store"; +import type { CachedTool, CacheOptions, CacheStats, CacheStore } from "./types"; /** * Default cache key generator - stable and deterministic */ function defaultKeyGenerator(params: any, context?: any): string { const paramsKey = serializeValue(params); - + if (context) { return `${paramsKey}|${context}`; } - + return paramsKey; } @@ -24,28 +22,34 @@ function defaultKeyGenerator(params: any, context?: any): string { function serializeValue(value: any): string { // Handle different parameter types like React Query if (value === null || value === undefined) { - return 'null'; + return "null"; } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { return String(value); } - + if (value instanceof Date) { return value.toISOString(); } - + if (Array.isArray(value)) { - return `[${value.map(serializeValue).join(',')}]`; + return `[${value.map(serializeValue).join(",")}]`; } - - if (typeof value === 'object') { + + if (typeof value === "object") { // Sort keys for deterministic serialization (like React Query) const sortedKeys = Object.keys(value).sort(); - const pairs = sortedKeys.map(key => `${key}:${serializeValue(value[key])}`); - return `{${pairs.join(',')}}`; + const pairs = sortedKeys.map( + (key) => `${key}:${serializeValue(value[key])}`, + ); + return `{${pairs.join(",")}}`; } - + return String(value); } @@ -54,7 +58,7 @@ function serializeValue(value: any): string { */ function createStreamingCachedTool( tool: T, - options: CacheOptions + options: CacheOptions, ): CachedTool { const { ttl = 5 * 60 * 1000, @@ -84,60 +88,67 @@ function createStreamingCachedTool( // Check cache first const cached = await cacheStore.get(key); - if (cached && (now - cached.timestamp) < ttl) { + if (cached && now - cached.timestamp < ttl) { hits++; onHit?.(key); - + const result = cached.result; - + if (debug) { const yields = result?.streamResults?.length || 0; const artifacts = result?.messages?.length || 0; const hasReturn = result?.returnValue !== undefined; - + console.log(`\n๐ŸŽฏ Cache HIT - Streaming Tool`); - console.log(`โ”Œโ”€ Key: ${key.slice(0, 60)}${key.length > 60 ? '...' : ''}`); + console.log( + `โ”Œโ”€ Key: ${key.slice(0, 60)}${key.length > 60 ? "..." : ""}`, + ); console.log(`โ”œโ”€ Streaming yields: ${yields}`); console.log(`โ”œโ”€ Artifact messages: ${artifacts}`); - console.log(`โ”œโ”€ Return value: ${hasReturn ? 'yes' : 'no'}`); + console.log(`โ”œโ”€ Return value: ${hasReturn ? "yes" : "no"}`); console.log(`โ””โ”€ Restoring cached results...\n`); } - + // Replay artifact messages first if (result?.messages?.length > 0) { - let writer = executionOptions?.writer || - (executionOptions as any)?.experimental_context?.writer; - + let writer = + executionOptions?.writer || + (executionOptions as any)?.experimental_context?.writer; + + // Writer comes from AI SDK's experimental_context if (!writer) { try { - const { artifacts } = await import('@ai-sdk-tools/artifacts'); - if (artifacts.isActive()) { - const context = artifacts.getContext(); - writer = context?.writer; - } + const { getWriter } = await import("@ai-sdk-tools/artifacts"); + writer = getWriter(executionOptions); } catch { - // Artifacts not available + // Artifacts package not available or writer not available } } - + if (writer) { - if (debug) console.log(` Replaying ${result.messages.length} artifact messages...`); + if (debug) + console.log( + ` Replaying ${result.messages.length} artifact messages...`, + ); for (const msg of result.messages) { writer.write(msg); } if (debug) console.log(` Artifacts restored`); } } - + // Replay streaming yields if (result?.streamResults) { - if (debug) console.log(` Replaying ${result.streamResults.length} streaming yields...`); + if (debug) + console.log( + ` Replaying ${result.streamResults.length} streaming yields...`, + ); for (const item of result.streamResults) { yield item; } if (debug) console.log(` Streaming content restored`); } - + return result.returnValue; } @@ -146,29 +157,32 @@ function createStreamingCachedTool( onMiss?.(key); if (debug) { console.log(`\n๐Ÿ”„ Cache MISS - Streaming Tool`); - console.log(`โ”Œโ”€ Key: ${key.slice(0, 60)}${key.length > 60 ? '...' : ''}`); - console.log(`โ”œโ”€ Will capture: streaming yields + artifact messages + return value`); + console.log( + `โ”Œโ”€ Key: ${key.slice(0, 60)}${key.length > 60 ? "..." : ""}`, + ); + console.log( + `โ”œโ”€ Will capture: streaming yields + artifact messages + return value`, + ); console.log(`โ””โ”€ Executing tool and capturing results...\n`); } // Capture writer messages - let writer = executionOptions?.writer || - (executionOptions as any)?.experimental_context?.writer; - + let writer = + executionOptions?.writer || + (executionOptions as any)?.experimental_context?.writer; + + // Writer comes from AI SDK's experimental_context if (!writer) { try { - const { artifacts } = await import('@ai-sdk-tools/artifacts'); - if (artifacts.isActive()) { - const context = artifacts.getContext(); - writer = context?.writer; - } + const { getWriter } = await import("@ai-sdk-tools/artifacts"); + writer = getWriter(executionOptions); } catch { - // Artifacts not available + // Artifacts package not available or writer not available } } - + const capturedMessages: any[] = []; - + if (writer) { const originalWrite = writer.write; writer.write = (data: any) => { @@ -179,31 +193,37 @@ function createStreamingCachedTool( // Execute original tool const originalResult = await tool.execute?.(params, executionOptions); - + // Create tee generator that streams and caches let lastChunk: any = null; - let finalReturnValue: any = undefined; + let finalReturnValue: any; let chunkCount = 0; - - if (originalResult && typeof originalResult[Symbol.asyncIterator] === 'function') { + + if ( + originalResult && + typeof originalResult[Symbol.asyncIterator] === "function" + ) { const iterator = originalResult[Symbol.asyncIterator](); let iterResult = await iterator.next(); - + while (!iterResult.done) { lastChunk = iterResult.value; // Just keep the last chunk (it has full text) chunkCount++; - + // Debug logging only for first few yields to avoid spam if (debug && chunkCount <= 3) { - console.log(` Capturing yield #${chunkCount}:`, lastChunk?.text?.slice(0, 40) + '...'); + console.log( + ` Capturing yield #${chunkCount}:`, + `${lastChunk?.text?.slice(0, 40)}...`, + ); } yield iterResult.value; // Stream immediately iterResult = await iterator.next(); } - + finalReturnValue = iterResult.value; } - + queueMicrotask(() => { // This runs after all current synchronous operations and promises queueMicrotask(async () => { @@ -213,9 +233,9 @@ function createStreamingCachedTool( streamResults: lastChunk ? [lastChunk] : [], // Only final chunk messages: capturedMessages, returnValue: finalReturnValue, - type: 'streaming' + type: "streaming", }; - + if (shouldCache(params, completeResult)) { await cacheStore.set(key, { result: completeResult, @@ -223,16 +243,21 @@ function createStreamingCachedTool( key, }); if (debug) { - const cacheItems = typeof cacheStore.size === 'function' ? await cacheStore.size() : 'unknown'; - + const cacheItems = + typeof cacheStore.size === "function" + ? await cacheStore.size() + : "unknown"; + // Calculate approximate memory usage const estimatedSize = JSON.stringify(completeResult).length; - const sizeKB = Math.round(estimatedSize / 1024 * 100) / 100; - + const sizeKB = Math.round((estimatedSize / 1024) * 100) / 100; + console.log(`\n๐Ÿ’พ Cache STORED - Streaming Tool`); console.log(`โ”Œโ”€ Streaming yields: ${chunkCount}`); console.log(`โ”œโ”€ Artifact messages: ${capturedMessages.length}`); - console.log(`โ”œโ”€ Return value: ${finalReturnValue !== undefined ? 'yes' : 'no'}`); + console.log( + `โ”œโ”€ Return value: ${finalReturnValue !== undefined ? "yes" : "no"}`, + ); console.log(`โ”œโ”€ Entry size: ~${sizeKB}KB`); console.log(`โ”œโ”€ Cache items: ${cacheItems}/${maxSize}`); console.log(`โ””โ”€ Ready for instant replay!\n`); @@ -243,7 +268,7 @@ function createStreamingCachedTool( } }); }); - + return finalReturnValue; }, getStats() { @@ -252,7 +277,10 @@ function createStreamingCachedTool( hits, misses, hitRate: total > 0 ? hits / total : 0, - size: typeof cacheStore.size === 'function' ? (cacheStore.size() as any) : 0, + size: + typeof cacheStore.size === "function" + ? (cacheStore.size() as any) + : 0, maxSize, }; }, @@ -268,15 +296,15 @@ function createStreamingCachedTool( const key = keyGenerator(params, context); const cached = await cacheStore.get(key); if (!cached) return false; - + const now = Date.now(); - const isValid = (now - cached.timestamp) < ttl; - + const isValid = now - cached.timestamp < ttl; + if (!isValid) { await cacheStore.delete(key); return false; } - + return true; }, getCacheKey(params: any) { @@ -288,10 +316,10 @@ function createStreamingCachedTool( export function cached( tool: T, - options?: CacheOptions + options?: CacheOptions, ): CachedTool { // For streaming tools, implement proper caching - if (tool.execute?.constructor?.name === 'AsyncGeneratorFunction') { + if (tool.execute?.constructor?.name === "AsyncGeneratorFunction") { return createStreamingCachedTool(tool, options || {}); } const { @@ -320,7 +348,10 @@ export function cached( hits, misses, hitRate: total > 0 ? hits / total : 0, - size: typeof cacheStore.size === 'function' ? (cacheStore.size() as any) : 0, + size: + typeof cacheStore.size === "function" + ? (cacheStore.size() as any) + : 0, maxSize, }; }, @@ -338,15 +369,15 @@ export function cached( const key = keyGenerator(params, context); const cached = await cacheStore.get(key); if (!cached) return false; - + const now = Date.now(); - const isValid = (now - cached.timestamp) < effectiveTTL; - + const isValid = now - cached.timestamp < effectiveTTL; + if (!isValid) { await cacheStore.delete(key); return false; } - + return true; }, @@ -358,125 +389,134 @@ export function cached( const cachedTool = new Proxy(tool, { get(target, prop) { - if (prop === 'execute') { + if (prop === "execute") { // Preserve the original function type - if (target.execute?.constructor?.name === 'AsyncGeneratorFunction') { + if (target.execute?.constructor?.name === "AsyncGeneratorFunction") { return async function* (...args: any[]) { - const [params, executionOptions] = args; - const context = cacheKey?.(); - const key = keyGenerator(params, context); - const now = Date.now(); - - // Check cache - const cached = await cacheStore.get(key); - if (cached && (now - cached.timestamp) < effectiveTTL) { - hits++; - onHit?.(key); - log(`[Cache] HIT`); - - const result = cached.result; - - // For streaming tools, replay messages immediately then return generator - if (target.execute?.constructor?.name === 'AsyncGeneratorFunction') { - // Replay messages IMMEDIATELY to restore artifact data - if (result?.messages?.length > 0) { - const writer = executionOptions?.writer || - (executionOptions as any)?.experimental_context?.writer; - - if (writer) { - log(`[Cache] Replaying ${result.messages.length} messages`); - for (const msg of result.messages) { - writer.write(msg); + const [params, executionOptions] = args; + const context = cacheKey?.(); + const key = keyGenerator(params, context); + const now = Date.now(); + + // Check cache + const cached = await cacheStore.get(key); + if (cached && now - cached.timestamp < effectiveTTL) { + hits++; + onHit?.(key); + log(`[Cache] HIT`); + + const result = cached.result; + + // For streaming tools, replay messages immediately then return generator + if ( + target.execute?.constructor?.name === "AsyncGeneratorFunction" + ) { + // Replay messages IMMEDIATELY to restore artifact data + if (result?.messages?.length > 0) { + const writer = + executionOptions?.writer || + (executionOptions as any)?.experimental_context?.writer; + + if (writer) { + log(`[Cache] Replaying ${result.messages.length} messages`); + for (const msg of result.messages) { + writer.write(msg); + } } } - } - - // Then return generator that yields stream results - return (async function* () { - if (result?.streamResults) { - for (const item of result.streamResults) { - yield item; - } - } else if (Array.isArray(result)) { - for (const item of result) { - yield item; + + // Then return generator that yields stream results + return (async function* () { + if (result?.streamResults) { + for (const item of result.streamResults) { + yield item; + } + } else if (Array.isArray(result)) { + for (const item of result) { + yield item; + } + } else { + yield result; } - } else { - yield result; - } - })(); + })(); + } + + return result; } - - return result; - } - // Execute original - misses++; - onMiss?.(key); - log(`[Cache] MISS`); - - // Capture messages if writer available - const writer = executionOptions?.writer || - (executionOptions as any)?.experimental_context?.writer; - - const capturedMessages: any[] = []; - - if (writer) { - const originalWrite = writer.write; - writer.write = (data: any) => { - capturedMessages.push(data); - return originalWrite.call(writer, data); - }; - } + // Execute original + misses++; + onMiss?.(key); + log(`[Cache] MISS`); - const result = await target.execute?.(params, executionOptions); - - // Handle streaming tools - if (result && typeof (result as any)[Symbol.asyncIterator] === 'function') { - const streamResults: any[] = []; - let lastChunk: any = null; - - // Stream to user immediately while capturing - const streamGenerator = (async function* () { - for await (const chunk of result as any) { - streamResults.push(chunk); - lastChunk = chunk; - yield chunk; // Stream immediately to user - } - - // After streaming completes, cache only the final chunk - queueMicrotask(async () => { - const completeResult = { - streamResults: lastChunk ? [lastChunk] : [], // Only store final chunk - messages: capturedMessages, - type: 'streaming' - }; - - if (shouldCache(params, completeResult)) { - await cacheStore.set(key, { - result: completeResult, - timestamp: now, - key, - }); - log(`[Cache] STORED streaming result with ${capturedMessages.length} messages`); + // Capture messages if writer available + const writer = + executionOptions?.writer || + (executionOptions as any)?.experimental_context?.writer; + + const capturedMessages: any[] = []; + + if (writer) { + const originalWrite = writer.write; + writer.write = (data: any) => { + capturedMessages.push(data); + return originalWrite.call(writer, data); + }; + } + + const result = await target.execute?.(params, executionOptions); + + // Handle streaming tools + if ( + result && + typeof (result as any)[Symbol.asyncIterator] === "function" + ) { + const streamResults: any[] = []; + let lastChunk: any = null; + + // Stream to user immediately while capturing + const streamGenerator = (async function* () { + for await (const chunk of result as any) { + streamResults.push(chunk); + lastChunk = chunk; + yield chunk; // Stream immediately to user } - }); - })(); - - return streamGenerator; - } - // Regular tool - if (shouldCache(params, result)) { - await cacheStore.set(key, { - result, - timestamp: now, - key, - }); - log(`[Cache] STORED result`); - } + // After streaming completes, cache only the final chunk + queueMicrotask(async () => { + const completeResult = { + streamResults: lastChunk ? [lastChunk] : [], // Only store final chunk + messages: capturedMessages, + type: "streaming", + }; + + if (shouldCache(params, completeResult)) { + await cacheStore.set(key, { + result: completeResult, + timestamp: now, + key, + }); + log( + `[Cache] STORED streaming result with ${capturedMessages.length} messages`, + ); + } + }); + })(); + + return streamGenerator; + } - return result; + // Regular tool + if (shouldCache(params, result)) { + await cacheStore.set(key, { + result, + timestamp: now, + key, + }); + log(`[Cache] STORED result`); + } + + return result; }; } else { // Regular async function @@ -488,7 +528,7 @@ export function cached( // Check cache const cached = await cacheStore.get(key); - if (cached && (now - cached.timestamp) < effectiveTTL) { + if (cached && now - cached.timestamp < effectiveTTL) { hits++; onHit?.(key); log(`[Cache] HIT`); @@ -515,13 +555,13 @@ export function cached( }; } } - + if (prop in cacheApi) { return cacheApi[prop as keyof typeof cacheApi]; } - + return target[prop as keyof typeof target]; - } + }, }) as unknown as CachedTool; return cachedTool; @@ -532,11 +572,11 @@ export function cached( */ export function createCachedFunction( store: CacheStore, - defaultOptions: Omit = {} + defaultOptions: Omit = {}, ) { return ( - tool: T, - options: Omit = {} + tool: T, + options: Omit = {}, ): CachedTool => { return cached(tool, { ...defaultOptions, ...options, store }); }; @@ -547,7 +587,7 @@ export function createCachedFunction( */ export function cacheTools>( tools: T, - options: CacheOptions = {} + options: CacheOptions = {}, ): { [K in keyof T]: CachedTool } { const cachedTools = {} as { [K in keyof T]: CachedTool }; @@ -558,35 +598,35 @@ export function cacheTools>( return cachedTools; } - - /** * Create a cached function with Redis client or default LRU - * + * * Example usage: * ```ts * import { Redis } from "@upstash/redis"; * import { createCached } from "@ai-sdk-tools/cache"; - * + * * // Upstash Redis * const cached = createCached({ cache: Redis.fromEnv() }); - * - * // Standard Redis + * + * // Standard Redis * const cached = createCached({ cache: Redis.createClient() }); - * + * * // Default LRU (no cache client) * const cached = createCached(); * ``` */ -export function createCached(options: { - cache?: any; // User's Redis client - we pass it directly - keyPrefix?: string; - ttl?: number; - debug?: boolean; - cacheKey?: () => string; - onHit?: (key: string) => void; - onMiss?: (key: string) => void; -} = {}) { +export function createCached( + options: { + cache?: any; // User's Redis client - we pass it directly + keyPrefix?: string; + ttl?: number; + debug?: boolean; + cacheKey?: () => string; + onHit?: (key: string) => void; + onMiss?: (key: string) => void; + } = {}, +) { // If no cache provided, use default LRU if (!options.cache) { const lruStore = createCacheBackend({ @@ -595,12 +635,12 @@ export function createCached(options: { defaultTTL: options.ttl || 10 * 60 * 1000, // 10 minutes default }); - return createCachedFunction(lruStore, { - debug: options.debug || false, - cacheKey: options.cacheKey, - onHit: options.onHit, - onMiss: options.onMiss, - }); + return createCachedFunction(lruStore, { + debug: options.debug || false, + cacheKey: options.cacheKey, + onHit: options.onHit, + onMiss: options.onMiss, + }); } // Use Redis client directly - no adapter needed! diff --git a/packages/devtools/README.md b/packages/devtools/README.md index 79e7111..709386e 100644 --- a/packages/devtools/README.md +++ b/packages/devtools/README.md @@ -68,25 +68,25 @@ function ChatComponent() { ## Features -### ๐ŸŽฏ Event Monitoring +### Event Monitoring - **Tool calls** - Start, result, and error events - **Message streaming** - Text chunks, completions, and deltas - **Step tracking** - Multi-step AI processes - **Error handling** - Capture and debug errors -### ๐Ÿ” Advanced Filtering +### Advanced Filtering - Filter by event type (tool calls, text events, errors, etc.) - Filter by tool name - Search through event data and metadata - Quick filter presets -### ๐Ÿ“Š Performance Metrics +### Performance Metrics - Real-time streaming speed (tokens/second) - Character streaming rate - Context window utilization - Event timing and duration -### ๐ŸŽจ Visual Interface +### Visual Interface - Resizable panel (drag to resize) - Live event indicators - Color-coded event types @@ -199,10 +199,10 @@ window.__AI_DEVTOOLS_DEBUG = true; - AI SDK React package - Modern browser with fetch API support -## License +## Contributing -MIT +Contributions are welcome! See the [contributing guide](../../CONTRIBUTING.md) for details. -## Contributing +## License -Contributions are welcome! Please feel free to submit a Pull Request. +MIT diff --git a/packages/devtools/src/styles.css b/packages/devtools/src/styles.css index 34e8b19..cbd9f87 100644 --- a/packages/devtools/src/styles.css +++ b/packages/devtools/src/styles.css @@ -47,157 +47,157 @@ /* Ensure devtools always appear on top */ .ai-devtools-panel, .ai-devtools-button { - z-index: 999999 !important; - position: fixed !important; + z-index: 999999; + position: fixed; } /* Button styles */ .ai-devtools-button { - bottom: 1rem !important; - right: 1rem !important; - width: 40px !important; - height: 40px !important; - background-color: #000000 !important; /* Pure black background */ - color: #ffffff !important; /* White text */ - border: 1px solid #333333 !important; /* Dark gray border */ - border-radius: 50% !important; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; - font-size: 10px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - transition: all 0.3s ease !important; - cursor: pointer !important; - padding: 0 !important; + bottom: 1rem; + right: 1rem; + width: 40px; + height: 40px; + background-color: #000000; /* Pure black background */ + color: #ffffff; /* White text */ + border: 1px solid #333333; /* Dark gray border */ + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + cursor: pointer; + padding: 0; } .ai-devtools-button-icon { - font-size: 1.75rem !important; - transition: all 0.3s ease !important; + font-size: 1.75rem; + transition: all 0.3s ease; } .ai-devtools-button:hover { - background-color: #1a1a1a !important; /* Slightly lighter black on hover */ - border-color: #555555 !important; /* Lighter border on hover */ + background-color: #1a1a1a; /* Slightly lighter black on hover */ + border-color: #555555; /* Lighter border on hover */ } /* Panel styles */ .ai-devtools-panel { - background-color: #000000 !important; /* Pure black background */ - color: #ffffff !important; /* White text */ - box-shadow: none !important; - font-size: 0.75rem !important; - line-height: 1rem !important; - overflow: visible !important; - position: fixed !important; - z-index: 2147483647 !important; - display: flex !important; - flex-direction: column !important; + background-color: #000000; /* Pure black background */ + color: #ffffff; /* White text */ + box-shadow: none; + font-size: 0.75rem; + line-height: 1rem; + overflow: visible; + position: fixed; + z-index: 2147483647; + display: flex; + flex-direction: column; } /* Bottom panel positioning */ .ai-devtools-panel-bottom { - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - border-top: 1px solid #333333 !important; /* Dark gray border */ + bottom: 0; + left: 0; + right: 0; + border-top: 1px solid #333333; /* Dark gray border */ } /* Right panel positioning */ .ai-devtools-panel-right { - top: 0 !important; - right: 0 !important; - bottom: 0 !important; - border-left: 1px solid #333333 !important; /* Dark gray border */ + top: 0; + right: 0; + bottom: 0; + border-left: 1px solid #333333; /* Dark gray border */ } /* Resize Handle */ .ai-devtools-resize-handle { - position: absolute !important; - background-color: transparent !important; - z-index: 2147483648 !important; + position: absolute; + background-color: transparent; + z-index: 2147483648; } /* Bottom panel resize handle */ .ai-devtools-resize-handle-bottom { - top: 0 !important; - left: 0 !important; - right: 0 !important; - height: 4px !important; - cursor: ns-resize !important; + top: 0; + left: 0; + right: 0; + height: 4px; + cursor: ns-resize; } /* Right panel resize handle */ .ai-devtools-resize-handle-right { - top: 0 !important; - left: 0 !important; - bottom: 0 !important; - width: 4px !important; - cursor: ew-resize !important; + top: 0; + left: 0; + bottom: 0; + width: 4px; + cursor: ew-resize; } /* Utility classes */ .ai-devtools .flex { - display: flex !important; + display: flex; } .ai-devtools .items-center { - align-items: center !important; + align-items: center; } .ai-devtools .justify-between { - justify-content: space-between !important; + justify-content: space-between; } .ai-devtools .px-3 { - padding-left: 0.75rem !important; - padding-right: 0.75rem !important; + padding-left: 0.75rem; + padding-right: 0.75rem; } .ai-devtools .py-1\.5 { - padding-top: 0.375rem !important; - padding-bottom: 0.375rem !important; + padding-top: 0.375rem; + padding-bottom: 0.375rem; } .ai-devtools .header-bg { - background-color: rgba(0, 0, 0, 0.8) !important; /* Black with opacity */ + background-color: rgba(0, 0, 0, 0.8); /* Black with opacity */ } .ai-devtools .border-bottom { - border-bottom-width: 1px !important; + border-bottom-width: 1px; } .ai-devtools .border-dark { - border-color: #333333 !important; /* Dark gray border */ + border-color: #333333; /* Dark gray border */ } .ai-devtools .text-small { - font-size: 0.75rem !important; - line-height: 1rem !important; + font-size: 0.75rem; + line-height: 1rem; } .ai-devtools .text-primary { - color: #ffffff !important; /* White for primary text */ + color: #ffffff; /* White for primary text */ } .ai-devtools .text-secondary { - color: #cccccc !important; /* Light gray for secondary text */ + color: #cccccc; /* Light gray for secondary text */ } .ai-devtools .text-muted { - color: #888888 !important; /* Medium gray for muted text */ + color: #888888; /* Medium gray for muted text */ } .ai-devtools .w-1 { - width: 0.25rem !important; + width: 0.25rem; } .ai-devtools .h-1 { - height: 0.25rem !important; + height: 0.25rem; } .ai-devtools .animate-pulse { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } @keyframes pulse { @@ -208,1239 +208,1239 @@ /* Button content styles */ .ai-devtools-button-icon { - width: 0.75rem !important; /* w-3 */ - height: 0.75rem !important; /* h-3 */ - margin-bottom: 0.125rem !important; /* mb-0.5 */ + width: 0.75rem; /* w-3 */ + height: 0.75rem; /* h-3 */ + margin-bottom: 0.125rem; /* mb-0.5 */ } .ai-devtools-button-count { - font-size: 8px !important; /* text-[8px] */ - line-height: 1 !important; /* leading-none */ + font-size: 8px; /* text-[8px] */ + line-height: 1; /* leading-none */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; /* font-mono */ + "Liberation Mono", "Courier New", monospace; /* font-mono */ } /* Header styles */ .ai-devtools-header { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - padding: 0.375rem 0.75rem !important; - background-color: rgba(0, 0, 0, 0.8) !important; /* Black with opacity */ - border-bottom: 1px solid #333333 !important; /* Dark gray border */ - overflow: visible !important; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0.75rem; + background-color: rgba(0, 0, 0, 0.8); /* Black with opacity */ + border-bottom: 1px solid #333333; /* Dark gray border */ + overflow: visible; } .ai-devtools-header-left { - display: flex !important; - align-items: center !important; - gap: 0.75rem !important; + display: flex; + align-items: center; + gap: 0.75rem; } .ai-devtools-context-circle-header { - margin-left: 0.75rem !important; - flex-shrink: 0 !important; - order: -1 !important; /* Place before other buttons */ + margin-left: 0.75rem; + flex-shrink: 0; + order: -1; /* Place before other buttons */ } .ai-devtools-count { - font-size: 10px !important; - color: #cccccc !important; /* Light gray text */ + font-size: 10px; + color: #cccccc; /* Light gray text */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } .ai-devtools-rec { - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; - font-size: 10px !important; - color: #ffffff !important; /* White text */ + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 10px; + color: #ffffff; /* White text */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } .ai-devtools-rec-dot { - width: 0.25rem !important; - height: 0.25rem !important; - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; - color: #cccccc !important; /* Light gray for recording dot */ + width: 0.25rem; + height: 0.25rem; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + color: #cccccc; /* Light gray for recording dot */ } /* Header right section */ .ai-devtools-header-right { - display: flex !important; - align-items: center !important; - gap: 0.75rem !important; + display: flex; + align-items: center; + gap: 0.75rem; } /* Main Search Bar */ .ai-devtools-search-bar { - position: relative !important; - display: flex !important; - align-items: center !important; - background-color: transparent !important; - border: none !important; - border-radius: 0 !important; - padding: 0 !important; - flex: 1 !important; - margin-right: 1rem !important; - min-height: 2.5rem !important; - overflow: visible !important; + position: relative; + display: flex; + align-items: center; + background-color: transparent; + border: none; + border-radius: 0; + padding: 0; + flex: 1; + margin-right: 1rem; + min-height: 2.5rem; + overflow: visible; } .ai-devtools-search-input-container { - display: flex !important; - align-items: center !important; - gap: 0.5rem !important; - width: 100% !important; - min-height: 2.5rem !important; - padding: 0.5rem 0.75rem !important; - background-color: transparent !important; - border: none !important; - border-right: 1px solid #333333 !important; - border-radius: 0 !important; - position: relative !important; + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + min-height: 2.5rem; + padding: 0.5rem 0.75rem; + background-color: transparent; + border: none; + border-right: 1px solid #333333; + border-radius: 0; + position: relative; } .ai-devtools-search-input-main { - flex: 1 !important; - background: transparent !important; - border: none !important; - outline: none !important; - color: #ffffff !important; + flex: 1; + background: transparent; + border: none; + outline: none; + color: #ffffff; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.875rem !important; - line-height: 1.2 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.875rem; + line-height: 1.2; } .ai-devtools-search-input-main::placeholder { - color: #555555 !important; - font-size: 0.75rem !important; + color: #555555; + font-size: 0.75rem; } /* Filter Chips */ .ai-devtools-filter-chip { - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; - padding: 0.25rem 0.5rem !important; - background-color: #333333 !important; - border: 1px solid #555555 !important; - border-radius: 0 !important; - color: #ffffff !important; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + background-color: #333333; + border: 1px solid #555555; + border-radius: 0; + color: #ffffff; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - white-space: nowrap !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + white-space: nowrap; } .ai-devtools-filter-chip-icon { - font-size: 0.75rem !important; - width: 0.875rem !important; - text-align: center !important; + font-size: 0.75rem; + width: 0.875rem; + text-align: center; } .ai-devtools-filter-chip-label { - font-weight: 500 !important; + font-weight: 500; } .ai-devtools-filter-chip-remove { - background: none !important; - border: none !important; - color: #cccccc !important; - cursor: pointer !important; - font-size: 1rem !important; - line-height: 1 !important; - padding: 0 !important; - margin-left: 0.25rem !important; - width: 1rem !important; - height: 1rem !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; + background: none; + border: none; + color: #cccccc; + cursor: pointer; + font-size: 1rem; + line-height: 1; + padding: 0; + margin-left: 0.25rem; + width: 1rem; + height: 1rem; + display: flex; + align-items: center; + justify-content: center; } .ai-devtools-filter-chip-remove:hover { - color: #ffffff !important; - background-color: #555555 !important; + color: #ffffff; + background-color: #555555; } /* Filter Indicator */ .ai-devtools-filter-indicator { - display: flex !important; - align-items: center !important; - justify-content: center !important; - padding: 0.25rem 0.5rem !important; - background-color: #333333 !important; - border: 1px solid #555555 !important; - color: #cccccc !important; + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.5rem; + background-color: #333333; + border: 1px solid #555555; + color: #cccccc; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - font-weight: 500 !important; - border-radius: 0 !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - flex-shrink: 0 !important; - min-width: 1.5rem !important; - height: 1.5rem !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + font-weight: 500; + border-radius: 0; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + min-width: 1.5rem; + height: 1.5rem; } .ai-devtools-filter-indicator:hover { - background-color: #444444 !important; - border-color: #666666 !important; - color: #ffffff !important; + background-color: #444444; + border-color: #666666; + color: #ffffff; } .ai-devtools-filter-indicator-count { - color: #ffffff !important; - font-size: 10px !important; - font-weight: 600 !important; - text-align: center !important; + color: #ffffff; + font-size: 10px; + font-weight: 600; + text-align: center; } /* Filter Badges */ .ai-devtools-filter-badges { - display: flex !important; - flex-direction: column !important; - gap: 0.5rem !important; - padding: 0.5rem 1rem !important; - background-color: #1a1a1a !important; - border-bottom: 1px solid #333333 !important; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem 1rem; + background-color: #1a1a1a; + border-bottom: 1px solid #333333; } .ai-devtools-filter-group { - display: flex !important; - align-items: center !important; - gap: 0.5rem !important; - flex-wrap: wrap !important; + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; } .ai-devtools-filter-group-label { - color: #888888 !important; + color: #888888; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - font-weight: 500 !important; - margin-right: 0.25rem !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + font-weight: 500; + margin-right: 0.25rem; } .ai-devtools-filter-badge { - background-color: #2a2a2a !important; - border: 1px solid #404040 !important; - color: #888888 !important; - padding: 0.25rem 0.5rem !important; + background-color: #2a2a2a; + border: 1px solid #404040; + color: #888888; + padding: 0.25rem 0.5rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - font-weight: 500 !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; - border-radius: 0 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.25rem; + border-radius: 0; } .ai-devtools-filter-badge:hover { - background-color: #404040 !important; - color: #ffffff !important; + background-color: #404040; + color: #ffffff; } .ai-devtools-filter-badge.active { - background-color: #404040 !important; - color: #ffffff !important; - border-color: #666666 !important; + background-color: #404040; + color: #ffffff; + border-color: #666666; } .ai-devtools-filter-badge.clear { - background-color: #ff4444 !important; - color: #ffffff !important; - border-color: #ff6666 !important; + background-color: #ff4444; + color: #ffffff; + border-color: #ff6666; } .ai-devtools-filter-badge.clear:hover { - background-color: #ff6666 !important; + background-color: #ff6666; } .ai-devtools-filter-remove { - color: #ff6666 !important; - font-weight: bold !important; - margin-left: 0.25rem !important; + color: #ff6666; + font-weight: bold; + margin-left: 0.25rem; } /* Filter Dropdown */ .ai-devtools-filter-dropdown { - position: absolute !important; - top: 100% !important; - left: 1rem !important; - right: 1rem !important; - background-color: #1a1a1a !important; - border: 1px solid #404040 !important; - border-top: none !important; - z-index: 2147483647 !important; - max-height: 400px !important; - overflow-y: auto !important; + position: absolute; + top: 100%; + left: 1rem; + right: 1rem; + background-color: #1a1a1a; + border: 1px solid #404040; + border-top: none; + z-index: 2147483647; + max-height: 400px; + overflow-y: auto; } .ai-devtools-filter-dropdown-content { - padding: 1rem !important; + padding: 1rem; } .ai-devtools-filter-section { - margin-bottom: 1.5rem !important; + margin-bottom: 1.5rem; } .ai-devtools-filter-section:last-child { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-filter-section-title { - color: #ffffff !important; + color: #ffffff; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.875rem !important; - font-weight: 600 !important; - margin-bottom: 0.75rem !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; } .ai-devtools-filter-options { - display: grid !important; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important; - gap: 0.5rem !important; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.5rem; } .ai-devtools-filter-option { - display: flex !important; - align-items: center !important; - gap: 0.5rem !important; - padding: 0.5rem 0.75rem !important; - background-color: #2a2a2a !important; - border: 1px solid #404040 !important; - color: #888888 !important; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: #2a2a2a; + border: 1px solid #404040; + color: #888888; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - text-align: left !important; - border-radius: 0 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; + text-align: left; + border-radius: 0; } .ai-devtools-filter-option:hover { - background-color: #404040 !important; - color: #ffffff !important; + background-color: #404040; + color: #ffffff; } .ai-devtools-filter-option.active { - background-color: #404040 !important; - color: #ffffff !important; - border-color: #666666 !important; + background-color: #404040; + color: #ffffff; + border-color: #666666; } .ai-devtools-filter-option-icon { - font-size: 0.875rem !important; - width: 1rem !important; - text-align: center !important; + font-size: 0.875rem; + width: 1rem; + text-align: center; } .ai-devtools-filter-option-label { - flex: 1 !important; - font-weight: 500 !important; + flex: 1; + font-weight: 500; } .ai-devtools-filter-option-count { - color: #666666 !important; - font-size: 0.6875rem !important; - background-color: #333333 !important; - padding: 0.125rem 0.375rem !important; - border-radius: 0 !important; + color: #666666; + font-size: 0.6875rem; + background-color: #333333; + padding: 0.125rem 0.375rem; + border-radius: 0; } .ai-devtools-filter-actions { - border-top: 1px solid #333333 !important; - padding-top: 1rem !important; - margin-top: 1rem !important; + border-top: 1px solid #333333; + padding-top: 1rem; + margin-top: 1rem; } .ai-devtools-filter-clear-all { - background-color: #ff4444 !important; - color: #ffffff !important; - border: 1px solid #ff6666 !important; - padding: 0.5rem 1rem !important; + background-color: #ff4444; + color: #ffffff; + border: 1px solid #ff6666; + padding: 0.5rem 1rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - font-weight: 500 !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - border-radius: 0 !important; - width: 100% !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 0; + width: 100%; } .ai-devtools-filter-clear-all:hover { - background-color: #ff6666 !important; + background-color: #ff6666; } /* Search Suggestions */ .ai-devtools-search-suggestions { - position: absolute !important; - top: calc(100% + 2px) !important; - left: 0 !important; - right: 0 !important; - background-color: #000000 !important; - border: 1px solid #333333 !important; - z-index: 2147483647 !important; - max-height: 300px !important; - overflow-y: auto !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.8) !important; - display: block !important; - visibility: visible !important; + position: absolute; + top: calc(100% + 2px); + left: 0; + right: 0; + background-color: #000000; + border: 1px solid #333333; + z-index: 2147483647; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.8); + display: block; + visibility: visible; } .ai-devtools-search-suggestions-content { - padding: 0 !important; - background-color: #000000 !important; + padding: 0; + background-color: #000000; } .ai-devtools-suggestion-section { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-suggestion-section:last-child { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-suggestion-section-title { - color: #666666 !important; + color: #666666; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.7rem !important; - font-weight: 600 !important; - margin: 0 !important; - padding: 0.5rem 0.75rem 0.25rem 0.75rem !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; - background-color: #111111 !important; - border-bottom: 1px solid #222222 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.7rem; + font-weight: 600; + margin: 0; + padding: 0.5rem 0.75rem 0.25rem 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + background-color: #111111; + border-bottom: 1px solid #222222; } .ai-devtools-suggestion-options { - display: flex !important; - flex-direction: column !important; - gap: 0 !important; + display: flex; + flex-direction: column; + gap: 0; } .ai-devtools-suggestion-option { - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; - padding: 0.5rem 0.75rem !important; - padding-right: 2.5rem !important; - background-color: transparent !important; - border: none !important; - border-bottom: 1px solid #222222 !important; - color: #cccccc !important; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + padding-right: 2.5rem; + background-color: transparent; + border: none; + border-bottom: 1px solid #222222; + color: #cccccc; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - cursor: pointer !important; - transition: all 0.15s ease !important; - text-align: left !important; - border-radius: 0 !important; - width: 100% !important; - position: relative !important; - overflow: hidden !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + border-radius: 0; + width: 100%; + position: relative; + overflow: hidden; } .ai-devtools-suggestion-option:hover { - background-color: #1a1a1a !important; - color: #ffffff !important; + background-color: #1a1a1a; + color: #ffffff; } .ai-devtools-suggestion-option.active { - background-color: #1a1a1a !important; - color: #ffffff !important; - border-left: 3px solid #666666 !important; + background-color: #1a1a1a; + color: #ffffff; + border-left: 3px solid #666666; } .ai-devtools-suggestion-icon { - font-size: 0.875rem !important; - width: 1rem !important; - text-align: center !important; + font-size: 0.875rem; + width: 1rem; + text-align: center; } .ai-devtools-suggestion-label { - flex: 1 !important; - font-weight: 500 !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - white-space: nowrap !important; - margin-right: 1.5rem !important; + flex: 1; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 1.5rem; } .ai-devtools-suggestion-count { - position: absolute !important; - right: 0.75rem !important; - top: 50% !important; - transform: translateY(-50%) !important; - color: #888888 !important; - font-size: 0.6875rem !important; - background-color: transparent !important; - padding: 0 !important; - border-radius: 0 !important; - font-weight: 400 !important; - min-width: 1.5rem !important; - text-align: right !important; + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: #888888; + font-size: 0.6875rem; + background-color: transparent; + padding: 0; + border-radius: 0; + font-weight: 400; + min-width: 1.5rem; + text-align: right; } .ai-devtools-suggestion-actions { - border-top: 1px solid #333333 !important; - padding-top: 1rem !important; - margin-top: 1rem !important; + border-top: 1px solid #333333; + padding-top: 1rem; + margin-top: 1rem; } .ai-devtools-suggestion-no-results { - color: #888888 !important; + color: #888888; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - text-align: center !important; - padding: 1rem !important; - font-style: italic !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + text-align: center; + padding: 1rem; + font-style: italic; } /* Button styles */ .ai-devtools-btn { - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; - padding: 0.125rem 0.375rem !important; - font-size: 10px !important; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.375rem; + font-size: 10px; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - border: 1px solid #333333 !important; /* Dark gray border */ - background-color: transparent !important; - color: #cccccc !important; /* Light gray text */ - cursor: pointer !important; - transition: all 0.15s ease !important; + "Liberation Mono", "Courier New", monospace; + border: 1px solid #333333; /* Dark gray border */ + background-color: transparent; + color: #cccccc; /* Light gray text */ + cursor: pointer; + transition: all 0.15s ease; } .ai-devtools-btn:hover { - background-color: #1a1a1a !important; /* Dark background on hover */ - border-color: #555555 !important; /* Lighter border on hover */ + background-color: #1a1a1a; /* Dark background on hover */ + border-color: #555555; /* Lighter border on hover */ } .ai-devtools-btn.active { - background-color: #333333 !important; /* Dark gray active background */ - color: #ffffff !important; /* White active text */ - border-color: #555555 !important; /* Lighter active border */ + background-color: #333333; /* Dark gray active background */ + color: #ffffff; /* White active text */ + border-color: #555555; /* Lighter active border */ } .ai-devtools-btn-icon { - width: 0.625rem !important; - height: 0.625rem !important; + width: 0.625rem; + height: 0.625rem; } .ai-devtools-close-btn { - display: flex !important; - align-items: center !important; - justify-content: center !important; - padding: 0.125rem !important; - color: #cccccc !important; /* Light gray text */ - cursor: pointer !important; - transition: color 0.15s ease !important; + display: flex; + align-items: center; + justify-content: center; + padding: 0.125rem; + color: #cccccc; /* Light gray text */ + cursor: pointer; + transition: color 0.15s ease; } .ai-devtools-close-btn:hover { - color: #ffffff !important; /* White on hover */ + color: #ffffff; /* White on hover */ } .ai-devtools-close-icon { - width: 0.75rem !important; - height: 0.75rem !important; + width: 0.75rem; + height: 0.75rem; } /* Position Toggle Button */ .ai-devtools-position-toggle-btn { - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 22px !important; - height: 22px !important; - padding: 0 !important; - font-size: 10px !important; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + font-size: 10px; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - border: 1px solid #333333 !important; /* Dark gray border */ - background-color: transparent !important; - color: #cccccc !important; /* Light gray text */ - cursor: pointer !important; - transition: all 0.15s ease !important; + "Liberation Mono", "Courier New", monospace; + border: 1px solid #333333; /* Dark gray border */ + background-color: transparent; + color: #cccccc; /* Light gray text */ + cursor: pointer; + transition: all 0.15s ease; } .ai-devtools-position-toggle-btn:hover { - background-color: #1a1a1a !important; /* Dark background on hover */ - border-color: #555555 !important; /* Lighter border on hover */ + background-color: #1a1a1a; /* Dark background on hover */ + border-color: #555555; /* Lighter border on hover */ } .ai-devtools-position-toggle-icon { - width: 12px !important; - height: 12px !important; + width: 12px; + height: 12px; } /* Content styles */ .ai-devtools-content { - display: flex !important; - height: 100% !important; - background-color: #000000 !important; /* Pure black background */ + display: flex; + height: 100%; + background-color: #000000; /* Pure black background */ } .ai-devtools-filters { - width: 20rem !important; - border-right: 1px solid #333333 !important; /* Dark gray border */ - overflow-y: auto !important; - background-color: rgba(0, 0, 0, 0.9) !important; /* Black with opacity */ + width: 20rem; + border-right: 1px solid #333333; /* Dark gray border */ + overflow-y: auto; + background-color: rgba(0, 0, 0, 0.9); /* Black with opacity */ } .ai-devtools-events { - flex: 1 !important; - overflow-y: auto !important; - padding: 0.5rem !important; - background-color: #000000 !important; /* Pure black background */ + flex: 1; + overflow-y: auto; + padding: 0.5rem; + background-color: #000000; /* Pure black background */ } /* Event item styles */ .ai-devtools-event-item { - border-bottom: 1px solid #222222 !important; /* Subtle border like terminal */ + border-bottom: 1px solid #222222; /* Subtle border like terminal */ } .ai-devtools-event-header { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - padding: 0.25rem 0.75rem !important; /* More compact padding */ - cursor: pointer !important; - transition: background-color 0.15s ease !important; - min-height: 1.5rem !important; /* More compact height */ - border-left: 2px solid transparent !important; /* Thinner left border */ - font-size: 0.75rem !important; /* Smaller text */ + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.75rem; /* More compact padding */ + cursor: pointer; + transition: background-color 0.15s ease; + min-height: 1.5rem; /* More compact height */ + border-left: 2px solid transparent; /* Thinner left border */ + font-size: 0.75rem; /* Smaller text */ } .ai-devtools-event-header:hover { - background-color: #1a1a1a !important; /* Dark hover background */ + background-color: #1a1a1a; /* Dark hover background */ } /* Color-coded left borders for different event types */ .ai-devtools-event-item[data-type="tool-call-result"] .ai-devtools-event-header { - border-left-color: #00ff00 !important; /* Green for success */ + border-left-color: #00ff00; /* Green for success */ } .ai-devtools-event-item[data-type="tool-call-error"] .ai-devtools-event-header { - border-left-color: #ff0000 !important; /* Red for errors */ + border-left-color: #ff0000; /* Red for errors */ } .ai-devtools-event-item[data-type="message-complete"] .ai-devtools-event-header { - border-left-color: #00ff00 !important; /* Green for completion */ + border-left-color: #00ff00; /* Green for completion */ } .ai-devtools-event-item[data-type="finish"] .ai-devtools-event-header { - border-left-color: #00ff00 !important; /* Green for finish */ + border-left-color: #00ff00; /* Green for finish */ } .ai-devtools-event-item[data-type="stream-done"] .ai-devtools-event-header { - border-left-color: #00ff00 !important; /* Green for stream done */ + border-left-color: #00ff00; /* Green for stream done */ } .ai-devtools-event-item[data-type="text-delta"] .ai-devtools-event-header { - border-left-color: #888888 !important; /* Gray for text deltas */ + border-left-color: #888888; /* Gray for text deltas */ } .ai-devtools-event-item[data-type="message-chunk"] .ai-devtools-event-header { - border-left-color: #888888 !important; /* Gray for chunks */ + border-left-color: #888888; /* Gray for chunks */ } .ai-devtools-event-item[data-type="start"] .ai-devtools-event-header { - border-left-color: #87ceeb !important; /* Light blue for start */ + border-left-color: #87ceeb; /* Light blue for start */ } .ai-devtools-event-item[data-type="reasoning-start"] .ai-devtools-event-header { - border-left-color: #9c27b0 !important; /* Purple for reasoning start */ + border-left-color: #9c27b0; /* Purple for reasoning start */ } .ai-devtools-event-item[data-type="reasoning-end"] .ai-devtools-event-header { - border-left-color: #9c27b0 !important; /* Purple for reasoning end */ + border-left-color: #9c27b0; /* Purple for reasoning end */ } .ai-devtools-event-content { - display: flex !important; - align-items: center !important; - gap: 0.75rem !important; /* More spacing for better readability */ - flex: 1 !important; - min-width: 0 !important; + display: flex; + align-items: center; + gap: 0.75rem; /* More spacing for better readability */ + flex: 1; + min-width: 0; } .ai-devtools-event-indicator { - width: 0.25rem !important; /* Slightly bigger indicator */ - height: 0.25rem !important; - border-radius: 50% !important; - flex-shrink: 0 !important; - background-color: #888888 !important; /* Medium gray */ - margin-right: 0.5rem !important; /* More spacing */ + width: 0.25rem; /* Slightly bigger indicator */ + height: 0.25rem; + border-radius: 50%; + flex-shrink: 0; + background-color: #888888; /* Medium gray */ + margin-right: 0.5rem; /* More spacing */ } .ai-devtools-event-icon { - font-size: 0.6875rem !important; /* Slightly smaller icon */ - flex-shrink: 0 !important; - color: #cccccc !important; /* Light gray */ - margin-right: 0.5rem !important; /* More spacing */ - font-weight: bold !important; /* Make icons more visible */ + font-size: 0.6875rem; /* Slightly smaller icon */ + flex-shrink: 0; + color: #cccccc; /* Light gray */ + margin-right: 0.5rem; /* More spacing */ + font-weight: bold; /* Make icons more visible */ } .ai-devtools-event-description { - flex: 1 !important; - min-width: 0 !important; - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.25rem; } .ai-devtools-event-text { - font-size: 0.75rem !important; /* Smaller text */ - color: #ffffff !important; /* White text */ - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; + font-size: 0.75rem; /* Smaller text */ + color: #ffffff; /* White text */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; /* Monospace font */ - line-height: 1.2 !important; /* Tighter line height */ - font-weight: 400 !important; /* Normal weight */ - letter-spacing: 0.025em !important; /* Slight letter spacing */ + "Liberation Mono", "Courier New", monospace; /* Monospace font */ + line-height: 1.2; /* Tighter line height */ + font-weight: 400; /* Normal weight */ + letter-spacing: 0.025em; /* Slight letter spacing */ } .ai-devtools-event-duration { - color: #888888 !important; /* Slightly dimmer for duration */ - font-size: 0.5625rem !important; /* Even smaller for duration */ + color: #888888; /* Slightly dimmer for duration */ + font-size: 0.5625rem; /* Even smaller for duration */ } /* Tool call session styles */ .ai-devtools-session { - border: 1px solid #2a2a2a !important; /* Lighter border */ - margin-bottom: 0.25rem !important; /* Tighter spacing */ - background-color: #0f0f0f !important; /* Slightly lighter background */ + border: 1px solid #2a2a2a; /* Lighter border */ + margin-bottom: 0.25rem; /* Tighter spacing */ + background-color: #0c0c0c0c; /* Slightly lighter background */ } .ai-devtools-session-header { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - padding: 0.375rem 0.75rem !important; /* More compact padding */ - cursor: pointer !important; - transition: background-color 0.15s ease !important; - background-color: #141414 !important; /* Slightly lighter header background */ - border-bottom: 1px solid #2a2a2a !important; /* Lighter border */ - min-height: 2rem !important; /* More compact height */ + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0.75rem; /* More compact padding */ + cursor: pointer; + transition: background-color 0.15s ease; + background-color: #141414; /* Slightly lighter header background */ + border-bottom: 1px solid #2a2a2a; /* Lighter border */ + min-height: 2rem; /* More compact height */ } .ai-devtools-session-header:hover { - background-color: #1a1a1a !important; /* Hover background */ + background-color: #1a1a1a; /* Hover background */ } .ai-devtools-session-content { - display: flex !important; - align-items: center !important; - gap: 0.5rem !important; - flex: 1 !important; + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; } .ai-devtools-session-indicator { - width: 0.375rem !important; /* Smaller indicator */ - height: 0.375rem !important; - border-radius: 50% !important; /* Round indicator */ - flex-shrink: 0 !important; - margin-right: 0.5rem !important; /* Add spacing */ + width: 0.375rem; /* Smaller indicator */ + height: 0.375rem; + border-radius: 50%; /* Round indicator */ + flex-shrink: 0; + margin-right: 0.5rem; /* Add spacing */ } /* Status-specific indicator colors */ .ai-devtools-session[data-status="completed"] .ai-devtools-session-indicator { - background-color: #00ff00 !important; /* Green for completed */ + background-color: #00ff00; /* Green for completed */ } .ai-devtools-session[data-status="running"] .ai-devtools-session-indicator { - background-color: #ffaa00 !important; /* Orange for running */ + background-color: #ffaa00; /* Orange for running */ } .ai-devtools-session[data-status="error"] .ai-devtools-session-indicator { - background-color: #ff0000 !important; /* Red for error */ + background-color: #ff0000; /* Red for error */ } .ai-devtools-session-icon { - font-size: 0.75rem !important; - flex-shrink: 0 !important; - color: #cccccc !important; - font-weight: bold !important; + font-size: 0.75rem; + flex-shrink: 0; + color: #cccccc; + font-weight: bold; } .ai-devtools-session-info { - flex: 1 !important; - min-width: 0 !important; + flex: 1; + min-width: 0; } .ai-devtools-session-tool-name { - font-size: 0.8125rem !important; /* Slightly smaller */ - color: #ffffff !important; - font-weight: 500 !important; /* Lighter weight for cleaner look */ + font-size: 0.8125rem; /* Slightly smaller */ + color: #ffffff; + font-weight: 500; /* Lighter weight for cleaner look */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - margin-bottom: 0.0625rem !important; /* Tighter spacing */ - letter-spacing: 0.025em !important; /* Slight letter spacing */ + "Liberation Mono", "Courier New", monospace; + margin-bottom: 0.0625rem; /* Tighter spacing */ + letter-spacing: 0.025em; /* Slight letter spacing */ } .ai-devtools-session-summary { - font-size: 0.6875rem !important; /* Smaller text */ - color: #999999 !important; /* Lighter gray */ + font-size: 0.6875rem; /* Smaller text */ + color: #999999; /* Lighter gray */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-weight: 400 !important; /* Normal weight */ + "Liberation Mono", "Courier New", monospace; + font-weight: 400; /* Normal weight */ } .ai-devtools-session-timestamp { - font-size: 0.6875rem !important; - color: #888888 !important; - flex-shrink: 0 !important; - margin-left: 0.75rem !important; + font-size: 0.6875rem; + color: #888888; + flex-shrink: 0; + margin-left: 0.75rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } .ai-devtools-session-expand { - margin-left: 0.5rem !important; - flex-shrink: 0 !important; + margin-left: 0.5rem; + flex-shrink: 0; } .ai-devtools-session-arrow { - width: 1rem !important; - height: 1rem !important; - color: #888888 !important; - transition: transform 0.15s ease !important; + width: 1rem; + height: 1rem; + color: #888888; + transition: transform 0.15s ease; } .ai-devtools-session-arrow-expanded { - transform: rotate(180deg) !important; + transform: rotate(180deg); } .ai-devtools-session-events { - background-color: #0a0a0a !important; /* Session events background */ - border-top: 1px solid #222222 !important; + background-color: #0a0a0a; /* Session events background */ + border-top: 1px solid #222222; } .ai-devtools-session-event { - border-left: 2px solid #333333 !important; /* Indent session events */ - margin-left: 1rem !important; - border-bottom: none !important; /* Remove individual event borders */ + border-left: 2px solid #333333; /* Indent session events */ + margin-left: 1rem; + border-bottom: none; /* Remove individual event borders */ } .ai-devtools-session-event:last-child { - border-bottom: 1px solid #222222 !important; /* Add border to last event */ + border-bottom: 1px solid #222222; /* Add border to last event */ } .ai-devtools-event-timestamp { - font-size: 0.6875rem !important; /* Slightly smaller timestamp */ - color: #888888 !important; /* Medium gray */ - flex-shrink: 0 !important; - margin-left: 0.75rem !important; /* More spacing */ + font-size: 0.6875rem; /* Slightly smaller timestamp */ + color: #888888; /* Medium gray */ + flex-shrink: 0; + margin-left: 0.75rem; /* More spacing */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; /* Monospace font */ - line-height: 1.2 !important; /* Slightly tighter line height */ + "Liberation Mono", "Courier New", monospace; /* Monospace font */ + line-height: 1.2; /* Slightly tighter line height */ } .ai-devtools-event-expand { - margin-left: 0.5rem !important; - flex-shrink: 0 !important; + margin-left: 0.5rem; + flex-shrink: 0; } .ai-devtools-event-arrow { - width: 0.75rem !important; - height: 0.75rem !important; - color: #888888 !important; /* Medium gray */ - transition: transform 0.15s ease !important; + width: 0.75rem; + height: 0.75rem; + color: #888888; /* Medium gray */ + transition: transform 0.15s ease; } .ai-devtools-event-arrow-expanded { - transform: rotate(180deg) !important; + transform: rotate(180deg); } .ai-devtools-event-expanded { - border-top: 1px solid #333333 !important; /* Dark gray border */ - background-color: rgba(0, 0, 0, 0.8) !important; /* Black with opacity */ + border-top: 1px solid #333333; /* Dark gray border */ + background-color: rgba(0, 0, 0, 0.8); /* Black with opacity */ } .ai-devtools-event-details { - padding: 0.75rem !important; + padding: 0.75rem; } .ai-devtools-event-metadata { - margin-bottom: 0.75rem !important; + margin-bottom: 0.75rem; } .ai-devtools-event-metadata-title { - font-size: 0.75rem !important; - font-weight: 500 !important; - color: #ffffff !important; /* White text */ - margin-bottom: 0.5rem !important; + font-size: 0.75rem; + font-weight: 500; + color: #ffffff; /* White text */ + margin-bottom: 0.5rem; } .ai-devtools-event-metadata-grid { - display: grid !important; - grid-template-columns: 1fr 1fr !important; - gap: 0.5rem !important; - font-size: 0.75rem !important; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + font-size: 0.75rem; } .ai-devtools-event-metadata-item { - color: #cccccc !important; /* Light gray */ + color: #cccccc; /* Light gray */ } .ai-devtools-event-metadata-label { - font-weight: 500 !important; - color: #ffffff !important; /* White text */ + font-weight: 500; + color: #ffffff; /* White text */ } .ai-devtools-event-data-title { - font-size: 0.75rem !important; - font-weight: 500 !important; - color: #ffffff !important; /* White text */ - margin-bottom: 0.5rem !important; + font-size: 0.75rem; + font-weight: 500; + color: #ffffff; /* White text */ + margin-bottom: 0.5rem; } .ai-devtools-event-data-content { - font-size: 0.75rem !important; - background-color: #1a1a1a !important; /* Dark background */ - padding: 0.5rem !important; - border: 1px solid #333333 !important; /* Dark gray border */ - overflow-x: auto !important; - max-height: 10rem !important; - overflow-y: auto !important; - color: #ffffff !important; /* White text */ + font-size: 0.75rem; + background-color: #1a1a1a; /* Dark background */ + padding: 0.5rem; + border: 1px solid #333333; /* Dark gray border */ + overflow-x: auto; + max-height: 10rem; + overflow-y: auto; + color: #ffffff; /* White text */ } .ai-devtools-event-metadata-section { - margin-top: 0.75rem !important; + margin-top: 0.75rem; } .ai-devtools-event-metadata-content { - font-size: 0.75rem !important; - background-color: #1a1a1a !important; /* Dark background */ - padding: 0.5rem !important; - border: 1px solid #333333 !important; /* Dark gray border */ - overflow-x: auto !important; - max-height: 8rem !important; - overflow-y: auto !important; - color: #ffffff !important; /* White text */ + font-size: 0.75rem; + background-color: #1a1a1a; /* Dark background */ + padding: 0.5rem; + border: 1px solid #333333; /* Dark gray border */ + overflow-x: auto; + max-height: 8rem; + overflow-y: auto; + color: #ffffff; /* White text */ } /* Panel content area - scrollable */ .ai-devtools-panel-content { - flex: 1 !important; - overflow-y: auto !important; - overflow-x: hidden !important; - display: flex !important; - flex-direction: column !important; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; } /* Event list styles */ .ai-devtools-event-list { - display: flex !important; - flex-direction: column !important; - gap: 0 !important; /* No gap between items like terminal */ + display: flex; + flex-direction: column; + gap: 0; /* No gap between items like terminal */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; /* Smaller, more compact text */ - line-height: 1.2 !important; /* Tighter line height */ + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; /* Smaller, more compact text */ + line-height: 1.2; /* Tighter line height */ } .ai-devtools-empty-state { margin-top: 1rem; - display: flex !important; - align-items: center !important; - justify-content: center !important; - height: 8rem !important; - color: #cccccc !important; /* Light gray */ + display: flex; + align-items: center; + justify-content: center; + height: 8rem; + color: #cccccc; /* Light gray */ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } .ai-devtools-empty-content { - text-align: center !important; + text-align: center; } .ai-devtools-empty-title { - font-size: 0.875rem !important; - margin-bottom: 0.5rem !important; - color: #ffffff !important; /* White text */ + font-size: 0.875rem; + margin-bottom: 0.5rem; + color: #ffffff; /* White text */ } .ai-devtools-empty-subtitle { - font-size: 0.75rem !important; - color: #888888 !important; /* Medium gray */ + font-size: 0.75rem; + color: #888888; /* Medium gray */ } /* Filter styles */ .ai-devtools-filters-container { - background-color: #000000 !important; /* Pure black background */ - border-bottom: 1px solid #333333 !important; /* Dark gray border */ + background-color: #000000; /* Pure black background */ + border-bottom: 1px solid #333333; /* Dark gray border */ } .ai-devtools-filters-content { - padding: 1rem !important; - display: flex !important; - flex-direction: column !important; - gap: 1rem !important; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .ai-devtools-filter-label { - display: block !important; - font-size: 0.875rem !important; - font-weight: 500 !important; - color: #ffffff !important; /* White text */ - margin-bottom: 0.5rem !important; + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #ffffff; /* White text */ + margin-bottom: 0.5rem; } .ai-devtools-search-container { - position: relative !important; + position: relative; } .ai-devtools-search-input { - width: 100% !important; - padding: 0.5rem 0.75rem !important; - border: 1px solid #333333 !important; /* Dark gray border */ - font-size: 0.875rem !important; - background-color: #1a1a1a !important; /* Dark background */ - color: #ffffff !important; /* White text */ - outline: none !important; - transition: border-color 0.15s ease !important; + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid #333333; /* Dark gray border */ + font-size: 0.875rem; + background-color: #1a1a1a; /* Dark background */ + color: #ffffff; /* White text */ + outline: none; + transition: border-color 0.15s ease; } .ai-devtools-search-input:focus { - border-color: #555555 !important; /* Lighter border on focus */ - box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.2) !important; /* Focus ring */ + border-color: #555555; /* Lighter border on focus */ + box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.2); /* Focus ring */ } .ai-devtools-search-clear { - position: absolute !important; - right: 0.5rem !important; - top: 50% !important; - transform: translateY(-50%) !important; - color: #888888 !important; /* Medium gray */ - background: none !important; - border: none !important; - cursor: pointer !important; - padding: 0.25rem !important; - transition: color 0.15s ease !important; + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + color: #888888; /* Medium gray */ + background: none; + border: none; + cursor: pointer; + padding: 0.25rem; + transition: color 0.15s ease; } .ai-devtools-search-clear:hover { - color: #cccccc !important; /* Light gray on hover */ + color: #cccccc; /* Light gray on hover */ } .ai-devtools-search-clear-icon { - width: 1rem !important; - height: 1rem !important; + width: 1rem; + height: 1rem; } .ai-devtools-filter-options { - display: flex !important; - flex-direction: column !important; - gap: 0.25rem !important; - max-height: 8rem !important; - overflow-y: auto !important; + display: flex; + flex-direction: column; + gap: 0.25rem; + max-height: 8rem; + overflow-y: auto; } .ai-devtools-filter-option { - display: flex !important; - align-items: center !important; - gap: 0.5rem !important; - padding: 0.5rem !important; - cursor: pointer !important; - transition: background-color 0.15s ease !important; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + cursor: pointer; + transition: background-color 0.15s ease; } .ai-devtools-filter-option:hover { - background-color: #1a1a1a !important; /* Dark hover background */ + background-color: #1a1a1a; /* Dark hover background */ } .ai-devtools-filter-option-selected { - background-color: #333333 !important; /* Dark selected background */ - border: 1px solid #555555 !important; /* Lighter border */ + background-color: #333333; /* Dark selected background */ + border: 1px solid #555555; /* Lighter border */ } .ai-devtools-checkbox { - width: 1rem !important; - height: 1rem !important; - color: #ffffff !important; /* White accent color */ - background-color: #1a1a1a !important; /* Dark background */ - border: 1px solid #333333 !important; /* Dark gray border */ - cursor: pointer !important; + width: 1rem; + height: 1rem; + color: #ffffff; /* White accent color */ + background-color: #1a1a1a; /* Dark background */ + border: 1px solid #333333; /* Dark gray border */ + cursor: pointer; } .ai-devtools-checkbox:checked { - background-color: #ffffff !important; /* White when checked */ - border-color: #ffffff !important; + background-color: #ffffff; /* White when checked */ + border-color: #ffffff; } .ai-devtools-type-indicator { - width: 0.75rem !important; - height: 0.75rem !important; - border-radius: 50% !important; - flex-shrink: 0 !important; + width: 0.75rem; + height: 0.75rem; + border-radius: 50%; + flex-shrink: 0; } .ai-devtools-type-icon { - font-size: 1.125rem !important; + font-size: 1.125rem; } .ai-devtools-type-label { - font-size: 0.875rem !important; - flex: 1 !important; - color: #ffffff !important; /* White text */ + font-size: 0.875rem; + flex: 1; + color: #ffffff; /* White text */ } .ai-devtools-type-count { - font-size: 0.75rem !important; - color: #888888 !important; /* Medium gray */ - background-color: #1a1a1a !important; /* Dark background */ - padding: 0.125rem 0.5rem !important; + font-size: 0.75rem; + color: #888888; /* Medium gray */ + background-color: #1a1a1a; /* Dark background */ + padding: 0.125rem 0.5rem; } .ai-devtools-tool-name { - font-size: 0.875rem !important; + font-size: 0.875rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - color: #ffffff !important; /* White text */ + "Liberation Mono", "Courier New", monospace; + color: #ffffff; /* White text */ } .ai-devtools-clear-filters { - padding-top: 0.5rem !important; - border-top: 1px solid #333333 !important; /* Dark gray border */ + padding-top: 0.5rem; + border-top: 1px solid #333333; /* Dark gray border */ } .ai-devtools-clear-button { - font-size: 0.875rem !important; - color: #cccccc !important; /* Light gray */ - background: none !important; - border: none !important; - cursor: pointer !important; - font-weight: 500 !important; - transition: color 0.15s ease !important; + font-size: 0.875rem; + color: #cccccc; /* Light gray */ + background: none; + border: none; + cursor: pointer; + font-weight: 500; + transition: color 0.15s ease; } .ai-devtools-clear-button:hover { - color: #ffffff !important; /* White on hover */ + color: #ffffff; /* White on hover */ } /* Tooltip styles */ .ai-devtools-tooltip-container { - position: relative !important; - display: inline-block !important; + position: relative; + display: inline-block; } .ai-devtools-tooltip { - position: absolute !important; - bottom: 100% !important; - left: 50% !important; - transform: translateX(-30%) !important; - margin-bottom: 8px !important; - padding: 0 !important; - background-color: #1a1a1a !important; - border: 1px solid #333333 !important; - border-radius: 4px !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.8) !important; - z-index: 2147483648 !important; - min-width: 200px !important; - max-width: 400px !important; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-30%); + margin-bottom: 8px; + padding: 0; + background-color: #1a1a1a; + border: 1px solid #333333; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.8); + z-index: 2147483648; + min-width: 200px; + max-width: 400px; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - line-height: 1.4 !important; - pointer-events: none !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + line-height: 1.4; + pointer-events: none; } .ai-devtools-tooltip::after { - content: "" !important; - position: absolute !important; - top: 100% !important; - left: 50% !important; - transform: translateX(-50%) !important; - border: 4px solid transparent !important; - border-top-color: #1a1a1a !important; + content: ""; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #1a1a1a; } .ai-devtools-tooltip-content { - padding: 0.75rem !important; + padding: 0.75rem; } .ai-devtools-tooltip-title { - color: #ffffff !important; - font-weight: 600 !important; - margin-bottom: 0.5rem !important; - font-size: 0.8rem !important; + color: #ffffff; + font-weight: 600; + margin-bottom: 0.5rem; + font-size: 0.8rem; } .ai-devtools-tooltip-params { - color: #cccccc !important; + color: #cccccc; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.7rem !important; - line-height: 1.3 !important; - margin: 0 !important; - white-space: pre-wrap !important; - word-break: break-word !important; - max-height: 200px !important; - overflow-y: auto !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.7rem; + line-height: 1.3; + margin: 0; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; } /* Parameters indicator */ .ai-devtools-session-params-indicator { - margin-left: 0.5rem !important; - font-size: 0.7rem !important; - opacity: 0.7 !important; - transition: opacity 0.2s ease !important; + margin-left: 0.5rem; + font-size: 0.7rem; + opacity: 0.7; + transition: opacity 0.2s ease; } .ai-devtools-session-header:hover .ai-devtools-session-params-indicator { - opacity: 1 !important; + opacity: 1; } /* Removed recPulse animation to prevent bleeding */ @@ -1482,14 +1482,14 @@ /* Enhanced button effects when receiving events */ .ai-devtools-button.receiving-events { - box-shadow: 0 0 20px rgba(34, 197, 94, 0.4) !important; - border-color: #333333 !important; - animation: buttonPulse 0.8s ease-in-out infinite !important; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.4); + border-color: #333333; + animation: buttonPulse 0.8s ease-in-out infinite; } .ai-devtools-button.receiving-events .ai-devtools-button-icon { - color: #22c55e !important; - animation: iconColorPulse 1.2s ease-in-out infinite !important; + color: #22c55e; + animation: iconColorPulse 1.2s ease-in-out infinite; } @keyframes buttonPulse { @@ -1529,249 +1529,249 @@ /* Context Insights Component */ .ai-devtools-context-insights { - display: flex !important; - align-items: center !important; - gap: 1rem !important; - padding: 0.5rem 0.75rem !important; - background-color: #111111 !important; - border: 1px solid #333333 !important; - border-radius: 0 !important; + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 0.75rem; + background-color: #111111; + border: 1px solid #333333; + border-radius: 0; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - color: #ffffff !important; - -webkit-font-smoothing: antialiased !important; - -moz-osx-font-smoothing: grayscale !important; - text-rendering: optimizeLegibility !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + color: #ffffff; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } /* Context Circle */ .ai-devtools-context-circle { - position: relative !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 100px !important; - height: 100px !important; - flex-shrink: 0 !important; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100px; + height: 100px; + flex-shrink: 0; } .ai-devtools-context-svg { - width: 100px !important; - height: 100px !important; - transform: rotate(-90deg) !important; + width: 100px; + height: 100px; + transform: rotate(-90deg); } .ai-devtools-context-bg { - stroke: #333333 !important; - stroke-width: 8 !important; - fill: none !important; + stroke: #333333; + stroke-width: 8; + fill: none; } .ai-devtools-context-progress { - stroke-width: 8 !important; - fill: none !important; - stroke-linecap: round !important; - transition: stroke-dashoffset 0.3s ease !important; + stroke-width: 8; + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 0.3s ease; } .ai-devtools-context-text { - position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - text-align: center !important; - z-index: 1 !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + z-index: 1; } .ai-devtools-context-percent { - font-size: 1.25rem !important; - font-weight: 600 !important; - color: #ffffff !important; - line-height: 1 !important; - margin-bottom: 0.125rem !important; + font-size: 1.25rem; + font-weight: 600; + color: #ffffff; + line-height: 1; + margin-bottom: 0.125rem; } .ai-devtools-context-label { - font-size: 0.625rem !important; - color: #888888 !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; - line-height: 1 !important; + font-size: 0.625rem; + color: #888888; + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; } /* Context Details */ .ai-devtools-context-details { - display: flex !important; - flex-direction: column !important; - gap: 0.25rem !important; - flex: 1 !important; - min-width: 0 !important; + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + min-width: 0; } .ai-devtools-context-row { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - gap: 0.5rem !important; - padding: 0.125rem 0 !important; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.125rem 0; } .ai-devtools-context-row.ai-devtools-context-warning { - color: #fbbf24 !important; - background-color: rgba(251, 191, 36, 0.1) !important; - padding: 0.25rem 0.5rem !important; - border-radius: 2px !important; - border: 1px solid rgba(251, 191, 36, 0.3) !important; + color: #fbbf24; + background-color: rgba(251, 191, 36, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 2px; + border: 1px solid rgba(251, 191, 36, 0.3); } .ai-devtools-context-row .ai-devtools-context-label { - font-size: 0.6875rem !important; - color: #888888 !important; - font-weight: 500 !important; - text-transform: uppercase !important; - letter-spacing: 0.025em !important; - flex-shrink: 0 !important; + font-size: 0.6875rem; + color: #888888; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-shrink: 0; } .ai-devtools-context-row .ai-devtools-context-value { - font-size: 0.75rem !important; - color: #ffffff !important; - font-weight: 600 !important; + font-size: 0.75rem; + color: #ffffff; + font-weight: 600; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - text-align: right !important; - flex-shrink: 0 !important; + "Liberation Mono", "Courier New", monospace; + text-align: right; + flex-shrink: 0; } /* Model Info */ .ai-devtools-model-info { - display: flex !important; - flex-direction: column !important; - gap: 0.125rem !important; - align-items: flex-end !important; - text-align: right !important; - flex-shrink: 0 !important; - min-width: 120px !important; + display: flex; + flex-direction: column; + gap: 0.125rem; + align-items: flex-end; + text-align: right; + flex-shrink: 0; + min-width: 120px; } .ai-devtools-model-name { - font-size: 0.75rem !important; - font-weight: 600 !important; - color: #ffffff !important; - line-height: 1.2 !important; - margin-bottom: 0.0625rem !important; + font-size: 0.75rem; + font-weight: 600; + color: #ffffff; + line-height: 1.2; + margin-bottom: 0.0625rem; } .ai-devtools-model-provider { - font-size: 0.625rem !important; - color: #888888 !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; - line-height: 1 !important; + font-size: 0.625rem; + color: #888888; + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; } /* Responsive adjustments */ @media (max-width: 768px) { .ai-devtools-context-insights { - flex-direction: column !important; - align-items: stretch !important; - gap: 0.75rem !important; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; } .ai-devtools-context-circle { - align-self: center !important; + align-self: center; } .ai-devtools-model-info { - align-items: center !important; - text-align: center !important; + align-items: center; + text-align: center; } } /* Compact Context Circle */ .ai-devtools-context-circle { - position: relative !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 36px !important; - height: 36px !important; - flex-shrink: 0 !important; - cursor: pointer !important; - transition: all 0.2s ease !important; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + flex-shrink: 0; + cursor: pointer; + transition: all 0.2s ease; } /* Hover scaling removed */ .ai-devtools-context-circle-compact { - position: relative !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 36px !important; - height: 36px !important; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; } .ai-devtools-context-svg-compact { - width: 36px !important; - height: 36px !important; - transform: rotate(-90deg) !important; + width: 36px; + height: 36px; + transform: rotate(-90deg); } .ai-devtools-context-bg-compact { - stroke: #333333 !important; - stroke-width: 2 !important; - fill: none !important; + stroke: #333333; + stroke-width: 2; + fill: none; } .ai-devtools-context-progress-compact { - stroke-width: 2 !important; - fill: none !important; - stroke-linecap: round !important; - transition: stroke-dashoffset 0.3s ease !important; + stroke-width: 2; + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 0.3s ease; } .ai-devtools-context-text-compact { - position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; - text-align: center !important; - z-index: 1 !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + z-index: 1; } .ai-devtools-context-percent-compact { - font-size: 0.625rem !important; - font-weight: 600 !important; - color: #ffffff !important; - line-height: 1 !important; + font-size: 0.625rem; + font-weight: 600; + color: #ffffff; + line-height: 1; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } /* Context Circle Tooltip */ .ai-devtools-context-tooltip { - position: absolute !important; - bottom: calc(100% + 8px) !important; - right: -20px !important; - transform: none !important; - background-color: #0a0a0a !important; - border: 1px solid #222222 !important; - border-radius: 0 !important; - box-shadow: none !important; - z-index: 2147483648 !important; - width: 200px !important; + position: absolute; + bottom: calc(100% + 8px); + right: -20px; + transform: none; + background-color: #0a0a0a; + border: 1px solid #222222; + border-radius: 0; + box-shadow: none; + z-index: 2147483648; + width: 200px; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.65rem !important; - line-height: 1.3 !important; - pointer-events: none !important; - opacity: 1 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.65rem; + line-height: 1.3; + pointer-events: none; + opacity: 1; } @keyframes contextTooltipFadeIn { @@ -1788,337 +1788,337 @@ /* Arrow removed */ .ai-devtools-context-tooltip-content { - padding: 1rem !important; + padding: 1rem; } /* Progress Section */ .ai-devtools-context-tooltip-progress-section { - margin-bottom: 0.75rem !important; + margin-bottom: 0.75rem; } .ai-devtools-context-tooltip-progress-header { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - margin-bottom: 0.5rem !important; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; } .ai-devtools-context-tooltip-percentage { - color: #ffffff !important; - font-weight: 600 !important; - font-size: 0.8rem !important; + color: #ffffff; + font-weight: 600; + font-size: 0.8rem; } .ai-devtools-context-tooltip-token-count { - color: #888888 !important; - font-size: 0.6rem !important; + color: #888888; + font-size: 0.6rem; } .ai-devtools-context-tooltip-progress-bar { - width: 100% !important; - height: 4px !important; - background-color: #404040 !important; - border-radius: 0 !important; - overflow: hidden !important; + width: 100%; + height: 4px; + background-color: #404040; + border-radius: 0; + overflow: hidden; } .ai-devtools-context-tooltip-progress-fill { - height: 100% !important; - background-color: #ffffff !important; - border-radius: 0 !important; - transition: width 0.3s ease !important; + height: 100%; + background-color: #ffffff; + border-radius: 0; + transition: width 0.3s ease; } /* Usage Section */ .ai-devtools-context-tooltip-usage-section { - margin-bottom: 0.75rem !important; - padding-top: 0.75rem !important; - border-top: 1px solid #404040 !important; + margin-bottom: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid #404040; } .ai-devtools-context-tooltip-usage-row { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - margin-bottom: 0.25rem !important; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.25rem; } .ai-devtools-context-tooltip-usage-row:last-child { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-context-tooltip-usage-label { - color: #888888 !important; - font-size: 0.6rem !important; + color: #888888; + font-size: 0.6rem; } .ai-devtools-context-tooltip-usage-value { - color: #ffffff !important; - font-size: 0.6rem !important; + color: #ffffff; + font-size: 0.6rem; } /* Cost Section */ .ai-devtools-context-tooltip-cost-section { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - padding-top: 0.75rem !important; - border-top: 1px solid #404040 !important; + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 0.75rem; + border-top: 1px solid #404040; } .ai-devtools-context-tooltip-cost-label { - color: #888888 !important; - font-size: 0.6rem !important; + color: #888888; + font-size: 0.6rem; } .ai-devtools-context-tooltip-cost-value { - color: #ffffff !important; - font-weight: 600 !important; - font-size: 0.6rem !important; + color: #ffffff; + font-weight: 600; + font-size: 0.6rem; } .ai-devtools-context-tooltip-section { - margin-bottom: 0.75rem !important; + margin-bottom: 0.75rem; } .ai-devtools-context-tooltip-section:last-child { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-context-tooltip-title { - color: #ffffff !important; - font-weight: 600 !important; - font-size: 0.7rem !important; - margin-bottom: 0.25rem !important; - line-height: 1.2 !important; + color: #ffffff; + font-weight: 600; + font-size: 0.7rem; + margin-bottom: 0.25rem; + line-height: 1.2; } .ai-devtools-context-tooltip-subtitle { - color: #888888 !important; - font-size: 0.6rem !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; - margin-bottom: 0.5rem !important; + color: #888888; + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; } .ai-devtools-context-tooltip-row { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - gap: 0.5rem !important; - padding: 0.125rem 0 !important; - font-size: 0.6rem !important; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.125rem 0; + font-size: 0.6rem; } .ai-devtools-context-tooltip-row.ai-devtools-context-tooltip-warning { - color: #fbbf24 !important; - background-color: rgba(251, 191, 36, 0.1) !important; - padding: 0.25rem 0.5rem !important; - border-radius: 3px !important; - border: 1px solid rgba(251, 191, 36, 0.3) !important; - margin: 0.25rem 0 !important; + color: #fbbf24; + background-color: rgba(251, 191, 36, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 3px; + border: 1px solid rgba(251, 191, 36, 0.3); + margin: 0.25rem 0; } .ai-devtools-context-tooltip-label { - color: #888888 !important; - font-weight: 500 !important; - text-transform: uppercase !important; - letter-spacing: 0.025em !important; - flex-shrink: 0 !important; + color: #888888; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-shrink: 0; } .ai-devtools-context-tooltip-value { - color: #ffffff !important; - font-weight: 600 !important; + color: #ffffff; + font-weight: 600; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - text-align: right !important; - flex-shrink: 0 !important; + "Liberation Mono", "Courier New", monospace; + text-align: right; + flex-shrink: 0; } /* Context Insights Demo */ .ai-devtools-context-insights-demo { - padding: 1rem !important; - background-color: #111111 !important; - border: 1px solid #333333 !important; - border-radius: 4px !important; + padding: 1rem; + background-color: #111111; + border: 1px solid #333333; + border-radius: 4px; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - color: #ffffff !important; + "Liberation Mono", "Courier New", monospace; + color: #ffffff; } .ai-devtools-context-insights-demo h3 { - font-size: 1rem !important; - font-weight: 600 !important; - color: #ffffff !important; - margin-bottom: 1rem !important; - text-align: center !important; + font-size: 1rem; + font-weight: 600; + color: #ffffff; + margin-bottom: 1rem; + text-align: center; } .ai-devtools-model-section { - margin-bottom: 1.5rem !important; - padding: 0.75rem !important; - background-color: #1a1a1a !important; - border: 1px solid #333333 !important; - border-radius: 4px !important; + margin-bottom: 1.5rem; + padding: 0.75rem; + background-color: #1a1a1a; + border: 1px solid #333333; + border-radius: 4px; } .ai-devtools-model-section:last-child { - margin-bottom: 0 !important; + margin-bottom: 0; } .ai-devtools-model-section h4 { - font-size: 0.875rem !important; - font-weight: 500 !important; - color: #cccccc !important; - margin-bottom: 0.5rem !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; + font-size: 0.875rem; + font-weight: 500; + color: #cccccc; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; } /* Bottom Stats Section */ .ai-devtools-bottom-stats { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - padding: 0.125rem 0.75rem !important; - border-top: 1px solid #333333 !important; - background-color: transparent !important; - min-height: 18px !important; - overflow: visible !important; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.125rem 0.75rem; + border-top: 1px solid #333333; + background-color: transparent; + min-height: 18px; + overflow: visible; } .ai-devtools-tokens-section { - display: flex !important; - align-items: center !important; + display: flex; + align-items: center; } .ai-devtools-context-section { - display: flex !important; - align-items: center !important; - overflow: visible !important; - padding: 0rem 1rem !important; + display: flex; + align-items: center; + overflow: visible; + padding: 0rem 1rem; } .ai-devtools-context-circle-bottom { - position: relative !important; - overflow: visible !important; + position: relative; + overflow: visible; } /* Streaming Speed Metrics */ .ai-devtools-speed-metrics { - display: flex !important; - gap: 1rem !important; - align-items: center !important; + display: flex; + gap: 1rem; + align-items: center; font-family: "Geist Mono", "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, - "Courier New", monospace !important; - font-size: 0.625rem !important; - color: #888888 !important; - -webkit-font-smoothing: antialiased !important; - -moz-osx-font-smoothing: grayscale !important; - text-rendering: optimizeLegibility !important; + "Courier New", monospace; + font-size: 0.625rem; + color: #888888; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } .ai-devtools-speed-metric { - display: flex !important; - align-items: center !important; - gap: 0.25rem !important; + display: flex; + align-items: center; + gap: 0.25rem; } .ai-devtools-speed-value { - font-weight: 600 !important; - color: #cccccc !important; - font-size: 0.625rem !important; - line-height: 1 !important; + font-weight: 600; + color: #cccccc; + font-size: 0.625rem; + line-height: 1; } .ai-devtools-speed-label { - font-size: 0.5rem !important; - color: #666666 !important; - line-height: 1 !important; + font-size: 0.5rem; + color: #666666; + line-height: 1; } /* Context Layout */ .ai-devtools-context-layout { - position: relative !important; - display: flex !important; - align-items: center !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - margin-right: 0.5rem !important; - gap: 0.375rem !important; - width: 60px !important; - height: 20px !important; + position: relative; + display: flex; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + margin-right: 0.5rem; + gap: 0.375rem; + width: 60px; + height: 20px; } .ai-devtools-context-percentage-text { font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.6rem !important; - font-weight: 400 !important; - color: #cccccc !important; - line-height: 1 !important; - width: 2.5rem !important; - text-align: right !important; - flex-shrink: 0 !important; - -webkit-font-smoothing: antialiased !important; - -moz-osx-font-smoothing: grayscale !important; - text-rendering: optimizeLegibility !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.6rem; + font-weight: 400; + color: #cccccc; + line-height: 1; + width: 2.5rem; + text-align: right; + flex-shrink: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } /* Small Context Circle */ .ai-devtools-context-circle-small { - position: relative !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - width: 20px !important; - height: 20px !important; - flex-shrink: 0 !important; - transition: all 0.2s ease !important; + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + flex-shrink: 0; + transition: all 0.2s ease; } .ai-devtools-context-svg-small { - width: 20px !important; - height: 20px !important; - transform: rotate(-90deg) !important; + width: 20px; + height: 20px; + transform: rotate(-90deg); } .ai-devtools-context-bg-small { - stroke: #666666 !important; - stroke-width: 1.5 !important; - fill: none !important; + stroke: #666666; + stroke-width: 1.5; + fill: none; } .ai-devtools-context-progress-small { - stroke: #cccccc !important; - stroke-width: 1.5 !important; - fill: none !important; - stroke-linecap: round !important; - transition: stroke-dashoffset 0.3s ease !important; + stroke: #cccccc; + stroke-width: 1.5; + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 0.3s ease; } /* State Watching Styles */ .ai-devtools-btn-badge { - position: absolute !important; - top: -4px !important; - right: -4px !important; - background: #ef4444 !important; - color: white !important; - font-size: 0.5rem !important; - font-weight: 600 !important; - padding: 0.125rem 0.25rem !important; - border-radius: 0.375rem !important; - min-width: 1rem !important; - height: 1rem !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - line-height: 1 !important; + position: absolute; + top: -4px; + right: -4px; + background: #ef4444; + color: white; + font-size: 0.5rem; + font-weight: 600; + padding: 0.125rem 0.25rem; + border-radius: 0.375rem; + min-width: 1rem; + height: 1rem; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; } .ai-devtools-state-panel { @@ -2146,512 +2146,519 @@ /* State Changes List */ .ai-devtools-state-changes-empty { - display: flex !important; - align-items: center !important; - justify-content: center !important; - height: 100% !important; - padding: 2rem !important; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem; } .ai-devtools-state-changes-empty-content { - text-align: center !important; - color: #666666 !important; + text-align: center; + color: #666666; } .ai-devtools-state-changes-empty-icon { - font-size: 2rem !important; - margin-bottom: 0.5rem !important; + font-size: 2rem; + margin-bottom: 0.5rem; } .ai-devtools-state-changes-empty-title { - font-size: 0.875rem !important; - font-weight: 600 !important; - margin-bottom: 0.25rem !important; - color: #cccccc !important; + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.25rem; + color: #cccccc; } .ai-devtools-state-changes-empty-description { - font-size: 0.75rem !important; - color: #888888 !important; + font-size: 0.75rem; + color: #888888; } .ai-devtools-state-changes-list { - display: flex !important; - flex-direction: column !important; - height: 100% !important; + display: flex; + flex-direction: column; + height: 100%; } .ai-devtools-state-changes-header { - padding: 0.75rem 1rem !important; - border-bottom: 1px solid #333333 !important; - background: #1a1a1a !important; + padding: 0.75rem 1rem; + border-bottom: 1px solid #333333; + background: #1a1a1a; } .ai-devtools-state-changes-title { - font-size: 0.875rem !important; - font-weight: 600 !important; - color: #cccccc !important; + font-size: 0.875rem; + font-weight: 600; + color: #cccccc; } .ai-devtools-state-changes-content { - flex: 1 !important; - overflow-y: auto !important; - padding: 0.5rem 0 !important; + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; } .ai-devtools-state-change-item { - padding: 0.75rem 1rem !important; - border-bottom: 1px solid #222222 !important; - cursor: pointer !important; - transition: background-color 0.2s ease !important; + padding: 0.75rem 1rem; + border-bottom: 1px solid #222222; + cursor: pointer; + transition: background-color 0.2s ease; } .ai-devtools-state-change-item:hover { - background: #222222 !important; + background: #222222; } .ai-devtools-state-change-item.selected { - background: #1e3a8a !important; + background: #1e3a8a; } .ai-devtools-state-change-header { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - margin-bottom: 0.5rem !important; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; } .ai-devtools-state-change-type { - display: flex !important; - align-items: center !important; - gap: 0.375rem !important; + display: flex; + align-items: center; + gap: 0.375rem; } .ai-devtools-state-change-type-icon { - font-size: 0.75rem !important; + font-size: 0.75rem; } .ai-devtools-state-change-type-label { - font-size: 0.75rem !important; - font-weight: 500 !important; - color: #cccccc !important; - text-transform: capitalize !important; + font-size: 0.75rem; + font-weight: 500; + color: #cccccc; + text-transform: capitalize; } .ai-devtools-state-change-timestamp { - font-size: 0.625rem !important; - color: #888888 !important; + font-size: 0.625rem; + color: #888888; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; + "Liberation Mono", "Courier New", monospace; } .ai-devtools-state-change-details { - display: flex !important; - flex-direction: column !important; - gap: 0.25rem !important; + display: flex; + flex-direction: column; + gap: 0.25rem; } .ai-devtools-state-change-store { - font-size: 0.625rem !important; - color: #888888 !important; + font-size: 0.625rem; + color: #888888; } .ai-devtools-state-change-store-id { - color: #cccccc !important; - font-weight: 500 !important; + color: #cccccc; + font-weight: 500; } .ai-devtools-state-change-keys { - font-size: 0.625rem !important; + font-size: 0.625rem; } .ai-devtools-state-change-keys-list { - display: flex !important; - flex-wrap: wrap !important; - gap: 0.25rem !important; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; } .ai-devtools-state-change-key { - background: #333333 !important; - color: #cccccc !important; - padding: 0.125rem 0.375rem !important; - border-radius: 0.25rem !important; + background: #333333; + color: #cccccc; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.5rem !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.5rem; } .ai-devtools-state-change-key-more { - color: #888888 !important; - font-style: italic !important; + color: #888888; + font-style: italic; } .ai-devtools-state-change-keys-none { - color: #666666 !important; - font-style: italic !important; + color: #666666; + font-style: italic; } /* State Data Explorer */ .ai-devtools-state-explorer-empty { - display: flex !important; - align-items: center !important; - justify-content: center !important; - height: 100% !important; - padding: 2rem !important; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem; } .ai-devtools-state-explorer-empty-content { - text-align: center !important; - color: #666666 !important; + text-align: center; + color: #666666; } .ai-devtools-state-explorer-empty-icon { - font-size: 2rem !important; - margin-bottom: 0.5rem !important; + font-size: 2rem; + margin-bottom: 0.5rem; } .ai-devtools-state-explorer-empty-title { - font-size: 0.875rem !important; - font-weight: 600 !important; - margin-bottom: 0.25rem !important; - color: #cccccc !important; + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.25rem; + color: #cccccc; } .ai-devtools-state-explorer-empty-description { - font-size: 0.75rem !important; - color: #888888 !important; + font-size: 0.75rem; + color: #888888; } .ai-devtools-state-explorer { - display: flex !important; - flex-direction: column !important; - height: 100% !important; + display: flex; + flex-direction: column; + height: 100%; } .ai-devtools-state-explorer-header { - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - padding: 0.75rem 1rem !important; - border-bottom: 1px solid #333333 !important; - background: #1a1a1a !important; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid #333333; + background: #1a1a1a; } .ai-devtools-state-explorer-title { - font-size: 0.875rem !important; - font-weight: 600 !important; - color: #cccccc !important; + font-size: 0.875rem; + font-weight: 600; + color: #cccccc; } .ai-devtools-state-explorer-subtitle { - color: #888888 !important; - font-weight: 400 !important; - margin-left: 0.5rem !important; + color: #888888; + font-weight: 400; + margin-left: 0.5rem; } .ai-devtools-state-explorer-modes { - display: flex !important; - gap: 0.25rem !important; + display: flex; + gap: 0.25rem; } .ai-devtools-state-explorer-mode-btn { - padding: 0.25rem 0.5rem !important; - font-size: 0.625rem !important; - border-radius: 0.25rem !important; - background: #333333 !important; - color: #888888 !important; - transition: all 0.2s ease !important; + padding: 0.25rem 0.5rem; + font-size: 0.625rem; + border-radius: 0.25rem; + background: #333333; + color: #888888; + transition: all 0.2s ease; } .ai-devtools-state-explorer-mode-btn:hover { - background: #444444 !important; - color: #cccccc !important; + background: #444444; + color: #cccccc; } .ai-devtools-state-explorer-mode-btn.active { - background: #1e3a8a !important; - color: #ffffff !important; + background: #1e3a8a; + color: #ffffff; } .ai-devtools-state-explorer-content { - flex: 1 !important; - overflow-y: auto !important; - padding: 1rem !important; + flex: 1; + overflow-y: auto; + padding: 1rem; font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace !important; - font-size: 0.75rem !important; - line-height: 1.4 !important; + "Liberation Mono", "Courier New", monospace; + font-size: 0.75rem; + line-height: 1.4; } /* JSON Viewer Styles */ .ai-devtools-json-null { - color: #888888 !important; - font-style: italic !important; + color: #888888; + font-style: italic; } .ai-devtools-json-string { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-primitive { - color: #888888 !important; + color: #888888; } .ai-devtools-json-array { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-array-content { - margin-left: 0.125rem !important; + margin-left: 0.125rem; } .ai-devtools-json-array-item { - margin-bottom: 0.25rem !important; - margin-left: 0 !important; - display: block !important; + margin-bottom: 0.25rem; + margin-left: 0; + display: block; } .ai-devtools-json-array-index { - color: #888888 !important; - margin-right: 0.5rem !important; + color: #888888; + margin-right: 0.5rem; } .ai-devtools-json-indent { - white-space: pre !important; - font-family: monospace !important; - display: inline !important; - flex-shrink: 0 !important; + white-space: pre; + font-family: monospace; + display: inline; + flex-shrink: 0; } .ai-devtools-json-inline { - display: inline !important; + display: inline; } .ai-devtools-json-object { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-object-content { - margin-left: 0.125rem !important; + margin-left: 0.125rem; } .ai-devtools-json-object-item { - margin-bottom: 0.25rem !important; - margin-left: 0 !important; - display: block !important; + margin-bottom: 0.25rem; + margin-left: 0; + display: block; } .ai-devtools-json-object-key-row { - display: flex !important; - align-items: flex-start !important; - gap: 0.25rem !important; + display: flex; + align-items: flex-start; + gap: 0.25rem; } .ai-devtools-json-expand-btn { - background: none !important; - border: none !important; - color: #888888 !important; - cursor: pointer !important; - padding: 0 !important; - margin: 0 !important; - margin-right: 0.25rem !important; - font-size: 0.75rem !important; - width: 1rem !important; - height: 1rem !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - flex-shrink: 0 !important; + background: none; + border: none; + color: #888888; + cursor: pointer; + padding: 0; + margin: 0; + margin-right: 0.25rem; + font-size: 0.75rem; + width: 1rem; + height: 1rem; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } .ai-devtools-json-expand-btn:hover { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-key { - color: #888888 !important; - margin-right: 0.5rem !important; - flex-shrink: 0 !important; + color: #888888; + margin-right: 0.5rem; + flex-shrink: 0; } .ai-devtools-json-preview { - color: #888888 !important; - font-style: italic !important; + color: #888888; + font-style: italic; } .ai-devtools-json-bracket { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-comma { - color: #888888 !important; - margin-left: 0.25rem !important; - display: inline !important; + color: #888888; + margin-left: 0.25rem; + display: inline; } .ai-devtools-json-truncated { - color: #888888 !important; - font-style: italic !important; - margin-left: 1rem !important; + color: #888888; + font-style: italic; + margin-left: 1rem; } .ai-devtools-json-unknown { - color: #888888 !important; + color: #888888; } /* React JSON View Lite Styles */ .ai-devtools-json-basic-child { - margin: 0 !important; - padding: 0 !important; + margin: 0; + padding: 0; } .ai-devtools-json-label { - color: #888888 !important; + color: #888888; } .ai-devtools-json-string-value { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-number-value { - color: #888888 !important; + color: #888888; } .ai-devtools-json-boolean-value { - color: #888888 !important; + color: #888888; } .ai-devtools-json-null-value { - color: #888888 !important; + color: #888888; } .ai-devtools-json-undefined-value { - color: #888888 !important; + color: #888888; } .ai-devtools-json-punctuation { - color: #cccccc !important; + color: #cccccc; } .ai-devtools-json-collapse-icon::after { - content: "โ–ผ" !important; - color: #cccccc !important; - margin-right: 0.25rem !important; + content: "โ–ผ"; + color: #cccccc; + margin-right: 0.25rem; } .ai-devtools-json-expand-icon::after { - content: "โ–ถ" !important; - color: #cccccc !important; - margin-right: 0.25rem !important; + content: "โ–ถ"; + color: #cccccc; + margin-right: 0.25rem; } .ai-devtools-json-collapsed-content::after { - content: "..." !important; - color: #888888 !important; + content: "..."; + color: #888888; } .ai-devtools-json-child-fields-container { - margin: 0 !important; - padding: 0 !important; - margin-left: 0.75rem !important; + margin: 0; + padding: 0; + margin-left: 0.75rem; } /* Additional indentation for nested levels */ -.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container { - margin-left: 1rem !important; +.ai-devtools-json-child-fields-container +.ai-devtools-json-child-fields-container { + margin-left: 1rem; } -.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container { - margin-left: 1.25rem !important; +.ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container { + margin-left: 1.25rem; } -.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container { - margin-left: 1.5rem !important; +.ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container { + margin-left: 1.5rem; } /* Ensure proper spacing for object and array items */ .ai-devtools-json-child-fields-container > * { - margin-bottom: 0.25rem !important; + margin-bottom: 0.25rem; } /* Improve visual hierarchy with better indentation */ .ai-devtools-json-basic-child { - margin: 0 !important; - padding: 0 !important; - position: relative !important; + margin: 0; + padding: 0; + position: relative; } /* Add visual indentation lines for better structure */ .ai-devtools-json-child-fields-container::before { - content: '' !important; - position: absolute !important; - left: -0.5rem !important; - top: 0 !important; - bottom: 0 !important; - width: 1px !important; - background: #444444 !important; - opacity: 0.3 !important; + content: ""; + position: absolute; + left: -0.5rem; + top: 0; + bottom: 0; + width: 1px; + background: #444444; + opacity: 0.3; } /* Style for object and array brackets */ .ai-devtools-json-punctuation { - color: #cccccc !important; - font-weight: normal !important; + color: #cccccc; + font-weight: normal; } /* Better spacing for array items */ .ai-devtools-json-child-fields-container .ai-devtools-json-basic-child { - margin-left: 0.25rem !important; + margin-left: 0.25rem; } /* Add subtle background for better readability */ .ai-devtools-json-child-fields-container { - background: rgba(255, 255, 255, 0.02) !important; - border-radius: 2px !important; - margin-top: 0.125rem !important; - margin-bottom: 0.125rem !important; + background: rgba(255, 255, 255, 0.02); + border-radius: 2px; + margin-top: 0.125rem; + margin-bottom: 0.125rem; } /* Improve the visual hierarchy with better spacing */ .ai-devtools-json-container { - font-family: monospace !important; - font-size: 12px !important; - line-height: 1.5 !important; - color: #cccccc !important; - background: transparent !important; - padding-left: 0.25rem !important; + font-family: monospace; + font-size: 12px; + line-height: 1.5; + color: #cccccc; + background: transparent; + padding-left: 0.25rem; } /* Better visual separation for different nesting levels */ .ai-devtools-json-child-fields-container:nth-child(odd) { - background: rgba(255, 255, 255, 0.01) !important; + background: rgba(255, 255, 255, 0.01); } /* Improve readability of nested content */ -.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container { - border-left: 1px solid rgba(255, 255, 255, 0.05) !important; - padding-left: 0.25rem !important; +.ai-devtools-json-child-fields-container +.ai-devtools-json-child-fields-container { + border-left: 1px solid rgba(255, 255, 255, 0.05); + padding-left: 0.25rem; } /* Add better visual hierarchy for deeply nested content */ -.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container { - border-left: 1px solid rgba(255, 255, 255, 0.08) !important; - background: rgba(255, 255, 255, 0.01) !important; +.ai-devtools-json-child-fields-container .ai-devtools-json-child-fields-container + .ai-devtools-json-child-fields-container { + border-left: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.01); } /* Ensure proper spacing between items */ .ai-devtools-json-child-fields-container > * + * { - margin-top: 0.125rem !important; + margin-top: 0.125rem; } /* Better visual separation for object keys */ .ai-devtools-json-label { - color: #888888 !important; - font-weight: 500 !important; - margin-right: 0.5rem !important; + color: #888888; + font-weight: 500; + margin-right: 0.5rem; } - diff --git a/packages/store/README.md b/packages/store/README.md index 767927b..77d21dd 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -2,15 +2,15 @@ A high-performance drop-in replacement for @ai-sdk/react with advanced state management, built-in optimizations, and zero configuration required. -## โšก Performance Features +## Performance Features -- ๐Ÿš€ **3-5x faster** than standard @ai-sdk/react -- ๐Ÿ” **O(1) message lookups** with hash map indexing -- ๐Ÿ“ฆ **Batched updates** to minimize re-renders -- ๐Ÿง  **Memoized selectors** with automatic caching -- ๐Ÿ“Š **Message virtualization** for large chat histories -- ๐ŸŽฏ **Advanced throttling** with scheduler.postTask -- ๐Ÿ”„ **Deep equality checks** to prevent unnecessary updates +- **3-5x faster** than standard @ai-sdk/react +- **O(1) message lookups** with hash map indexing +- **Batched updates** to minimize re-renders +- **Memoized selectors** with automatic caching +- **Message virtualization** for large chat histories +- **Advanced throttling** with scheduler.postTask +- **Deep equality checks** to prevent unnecessary updates ## Installation @@ -209,6 +209,10 @@ const chat = useChat({ const messages = useChatMessages() // Fully typed! ``` +## Contributing + +Contributions are welcome! See the [contributing guide](../../CONTRIBUTING.md) for details. + ## License MIT \ No newline at end of file