mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
Merge branch 'dev' of github.com:rowboatlabs/rowboat into dev
This commit is contained in:
commit
96e2625c6e
15 changed files with 648 additions and 124 deletions
|
|
@ -33,9 +33,10 @@ import {
|
|||
usePromptInputController,
|
||||
type FileMention,
|
||||
} from '@/components/ai-elements/prompt-input';
|
||||
import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning';
|
||||
|
||||
import { Shimmer } from '@/components/ai-elements/shimmer';
|
||||
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool';
|
||||
import { WebSearchResult } from '@/components/ai-elements/web-search-result';
|
||||
import { PermissionRequest } from '@/components/ai-elements/permission-request';
|
||||
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request';
|
||||
import { Suggestions } from '@/components/ai-elements/suggestions';
|
||||
|
|
@ -54,6 +55,7 @@ import { FileCardProvider } from '@/contexts/file-card-context'
|
|||
import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
|
||||
import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
|
||||
import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
|
||||
import { toast } from "sonner"
|
||||
|
||||
type DirEntry = z.infer<typeof workspace.DirEntry>
|
||||
type RunEventType = z.infer<typeof RunEvent>
|
||||
|
|
@ -80,20 +82,20 @@ interface ToolCall {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ReasoningBlock {
|
||||
interface ErrorMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
kind: 'error';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type ConversationItem = ChatMessage | ToolCall | ReasoningBlock;
|
||||
type ConversationItem = ChatMessage | ToolCall | ErrorMessage;
|
||||
|
||||
type ToolState = 'input-streaming' | 'input-available' | 'output-available' | 'output-error';
|
||||
|
||||
const isChatMessage = (item: ConversationItem): item is ChatMessage => 'role' in item
|
||||
const isToolCall = (item: ConversationItem): item is ToolCall => 'name' in item
|
||||
const isReasoningBlock = (item: ConversationItem): item is ReasoningBlock =>
|
||||
'content' in item && !('role' in item) && !('name' in item)
|
||||
const isErrorMessage = (item: ConversationItem): item is ErrorMessage => 'kind' in item && item.kind === 'error'
|
||||
|
||||
const toToolState = (status: ToolCall['status']): ToolState => {
|
||||
switch (status) {
|
||||
|
|
@ -642,7 +644,6 @@ function App() {
|
|||
const [message, setMessage] = useState<string>('')
|
||||
const [conversation, setConversation] = useState<ConversationItem[]>([])
|
||||
const [currentAssistantMessage, setCurrentAssistantMessage] = useState<string>('')
|
||||
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
||||
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
||||
const [runId, setRunId] = useState<string | null>(null)
|
||||
const runIdRef = useRef<string | null>(null)
|
||||
|
|
@ -650,7 +651,7 @@ function App() {
|
|||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [processingRunIds, setProcessingRunIds] = useState<Set<string>>(new Set())
|
||||
const processingRunIdsRef = useRef<Set<string>>(new Set())
|
||||
const streamingBuffersRef = useRef<Map<string, { assistant: string; reasoning: string }>>(new Map())
|
||||
const streamingBuffersRef = useRef<Map<string, { assistant: string }>>(new Map())
|
||||
const [isStopping, setIsStopping] = useState(false)
|
||||
const [stopClickedAt, setStopClickedAt] = useState<number | null>(null)
|
||||
const [agentId] = useState<string>('copilot')
|
||||
|
|
@ -722,7 +723,6 @@ function App() {
|
|||
if (!runId) {
|
||||
setIsProcessing(false)
|
||||
setCurrentAssistantMessage('')
|
||||
setCurrentReasoning('')
|
||||
return
|
||||
}
|
||||
const isRunProcessing = processingRunIdsRef.current.has(runId)
|
||||
|
|
@ -730,10 +730,8 @@ function App() {
|
|||
if (isRunProcessing) {
|
||||
const buffer = streamingBuffersRef.current.get(runId)
|
||||
setCurrentAssistantMessage(buffer?.assistant ?? '')
|
||||
setCurrentReasoning(buffer?.reasoning ?? '')
|
||||
} else {
|
||||
setCurrentAssistantMessage('')
|
||||
setCurrentReasoning('')
|
||||
streamingBuffersRef.current.delete(runId)
|
||||
}
|
||||
}, [runId])
|
||||
|
|
@ -1113,6 +1111,15 @@ function App() {
|
|||
}
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
items.push({
|
||||
id: `error-${Date.now()}-${Math.random()}`,
|
||||
kind: 'error',
|
||||
message: event.error,
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'llm-stream-event': {
|
||||
// We don't need to reconstruct streaming events for history
|
||||
// Reasoning is captured in the final message
|
||||
|
|
@ -1182,15 +1189,15 @@ function App() {
|
|||
const getStreamingBuffer = (id: string) => {
|
||||
const existing = streamingBuffersRef.current.get(id)
|
||||
if (existing) return existing
|
||||
const next = { assistant: '', reasoning: '' }
|
||||
const next = { assistant: '' }
|
||||
streamingBuffersRef.current.set(id, next)
|
||||
return next
|
||||
}
|
||||
|
||||
const appendStreamingBuffer = (id: string, field: 'assistant' | 'reasoning', delta: string) => {
|
||||
const appendStreamingBuffer = (id: string, delta: string) => {
|
||||
if (!delta) return
|
||||
const buffer = getStreamingBuffer(id)
|
||||
buffer[field] += delta
|
||||
buffer.assistant += delta
|
||||
}
|
||||
|
||||
const clearStreamingBuffer = (id: string) => {
|
||||
|
|
@ -1231,7 +1238,6 @@ function App() {
|
|||
case 'start':
|
||||
if (!isActiveRun) return
|
||||
setCurrentAssistantMessage('')
|
||||
setCurrentReasoning('')
|
||||
setModelUsage(null)
|
||||
break
|
||||
|
||||
|
|
@ -1239,29 +1245,13 @@ function App() {
|
|||
{
|
||||
const llmEvent = event.event
|
||||
if (!isActiveRun) {
|
||||
if (llmEvent.type === 'reasoning-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, 'reasoning', llmEvent.delta)
|
||||
} else if (llmEvent.type === 'text-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, 'assistant', llmEvent.delta)
|
||||
if (llmEvent.type === 'text-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, llmEvent.delta)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (llmEvent.type === 'reasoning-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, 'reasoning', llmEvent.delta)
|
||||
setCurrentReasoning(prev => prev + llmEvent.delta)
|
||||
} else if (llmEvent.type === 'reasoning-end') {
|
||||
setCurrentReasoning(reasoning => {
|
||||
if (reasoning) {
|
||||
setConversation(prev => [...prev, {
|
||||
id: `reasoning-${Date.now()}`,
|
||||
content: reasoning,
|
||||
timestamp: Date.now(),
|
||||
}])
|
||||
}
|
||||
return ''
|
||||
})
|
||||
} else if (llmEvent.type === 'text-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, 'assistant', llmEvent.delta)
|
||||
if (llmEvent.type === 'text-delta' && llmEvent.delta) {
|
||||
appendStreamingBuffer(event.runId, llmEvent.delta)
|
||||
setCurrentAssistantMessage(prev => prev + llmEvent.delta)
|
||||
} else if (llmEvent.type === 'tool-call') {
|
||||
setConversation(prev => [...prev, {
|
||||
|
|
@ -1454,7 +1444,6 @@ function App() {
|
|||
}
|
||||
return ''
|
||||
})
|
||||
setCurrentReasoning('')
|
||||
break
|
||||
|
||||
case 'error':
|
||||
|
|
@ -1468,6 +1457,13 @@ function App() {
|
|||
setIsProcessing(false)
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
setConversation(prev => [...prev, {
|
||||
id: `error-${Date.now()}`,
|
||||
kind: 'error',
|
||||
message: event.error,
|
||||
timestamp: Date.now(),
|
||||
}])
|
||||
toast.error(event.error.split('\n')[0] || 'Model error')
|
||||
console.error('Run error:', event.error)
|
||||
break
|
||||
}
|
||||
|
|
@ -1602,7 +1598,6 @@ function App() {
|
|||
loadRunRequestIdRef.current += 1
|
||||
setConversation([])
|
||||
setCurrentAssistantMessage('')
|
||||
setCurrentReasoning('')
|
||||
setRunId(null)
|
||||
setMessage('')
|
||||
setModelUsage(null)
|
||||
|
|
@ -2203,6 +2198,39 @@ function App() {
|
|||
}
|
||||
|
||||
if (isToolCall(item)) {
|
||||
if (item.name === 'web-search') {
|
||||
const input = normalizeToolInput(item.input) as Record<string, unknown> | undefined
|
||||
const result = item.result as Record<string, unknown> | undefined
|
||||
return (
|
||||
<WebSearchResult
|
||||
key={item.id}
|
||||
query={(input?.query as string) || ''}
|
||||
results={(result?.results as Array<{ title: string; url: string; description: string }>) || []}
|
||||
status={item.status}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (item.name === 'research-search') {
|
||||
const input = normalizeToolInput(item.input) as Record<string, unknown> | undefined
|
||||
const result = item.result as Record<string, unknown> | undefined
|
||||
const rawResults = (result?.results as Array<{ title: string; url: string; highlights?: string[]; text?: string }>) || []
|
||||
const mapped = rawResults.map(r => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
description: r.highlights?.[0] || (r.text ? r.text.slice(0, 200) : ''),
|
||||
}))
|
||||
const category = input?.category as string | undefined
|
||||
const cardTitle = category ? `${category.charAt(0).toUpperCase() + category.slice(1)} search` : 'Researched the web'
|
||||
return (
|
||||
<WebSearchResult
|
||||
key={item.id}
|
||||
query={(input?.query as string) || ''}
|
||||
results={mapped}
|
||||
status={item.status}
|
||||
title={cardTitle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const errorText = item.status === 'error' ? 'Tool error' : ''
|
||||
const output = normalizeToolOutput(item.result, item.status)
|
||||
const input = normalizeToolInput(item.input)
|
||||
|
|
@ -2223,19 +2251,20 @@ function App() {
|
|||
)
|
||||
}
|
||||
|
||||
if (isReasoningBlock(item)) {
|
||||
if (isErrorMessage(item)) {
|
||||
return (
|
||||
<Reasoning key={item.id}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{item.content}</ReasoningContent>
|
||||
</Reasoning>
|
||||
<Message key={item.id} from="assistant">
|
||||
<MessageContent className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-destructive">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs">{item.message}</pre>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning
|
||||
const hasConversation = conversation.length > 0 || currentAssistantMessage
|
||||
const conversationContentClassName = hasConversation
|
||||
? "mx-auto w-full max-w-4xl pb-28"
|
||||
: "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0"
|
||||
|
|
@ -2447,13 +2476,6 @@ function App() {
|
|||
/>
|
||||
))}
|
||||
|
||||
{currentReasoning && (
|
||||
<Reasoning isStreaming>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{currentReasoning}</ReasoningContent>
|
||||
</Reasoning>
|
||||
)}
|
||||
|
||||
{currentAssistantMessage && (
|
||||
<Message from="assistant">
|
||||
<MessageContent>
|
||||
|
|
@ -2462,7 +2484,7 @@ function App() {
|
|||
</Message>
|
||||
)}
|
||||
|
||||
{isProcessing && !currentAssistantMessage && !currentReasoning && (
|
||||
{isProcessing && !currentAssistantMessage && (
|
||||
<Message from="assistant">
|
||||
<MessageContent>
|
||||
<Shimmer duration={1}>Thinking...</Shimmer>
|
||||
|
|
@ -2508,7 +2530,6 @@ function App() {
|
|||
onOpenFullScreen={navigateToFullScreenChat}
|
||||
conversation={conversation}
|
||||
currentAssistantMessage={currentAssistantMessage}
|
||||
currentReasoning={currentReasoning}
|
||||
isProcessing={isProcessing}
|
||||
isStopping={isStopping}
|
||||
onStop={handleStop}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
GlobeIcon,
|
||||
LoaderIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
interface WebSearchResultProps {
|
||||
query: string;
|
||||
results: Array<{ title: string; url: string; description: string }>;
|
||||
status: "pending" | "running" | "completed" | "error";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function getDomain(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export function WebSearchResult({ query, results, status, title = "Searched the web" }: WebSearchResultProps) {
|
||||
const isRunning = status === "pending" || status === "running";
|
||||
|
||||
return (
|
||||
<Collapsible defaultOpen className="not-prose mb-4 w-full rounded-md border">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between gap-4 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<GlobeIcon className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
</div>
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="px-3 pb-3 space-y-3">
|
||||
{/* Query + result count */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground min-w-0">
|
||||
<GlobeIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{query}</span>
|
||||
</div>
|
||||
{results.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{results.length} result{results.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results list */}
|
||||
{results.length > 0 && (
|
||||
<div className="rounded-md border max-h-64 overflow-y-auto">
|
||||
{results.map((result, index) => {
|
||||
const domain = getDomain(result.url);
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(result.url, "_blank");
|
||||
}}
|
||||
className="flex items-center justify-between gap-3 px-3 py-2 text-sm hover:bg-muted/50 transition-colors border-b last:border-b-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<img
|
||||
src={`https://www.google.com/s2/favicons?domain=${domain}&sz=16`}
|
||||
alt=""
|
||||
className="size-4 shrink-0"
|
||||
/>
|
||||
<span className="truncate">{result.title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap shrink-0">
|
||||
{domain}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{isRunning ? (
|
||||
<>
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
<span>Searching...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircleIcon className="size-3.5 text-green-600" />
|
||||
<span>Done</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
MessageContent,
|
||||
MessageResponse,
|
||||
} from '@/components/ai-elements/message'
|
||||
import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning'
|
||||
|
||||
import { Shimmer } from '@/components/ai-elements/shimmer'
|
||||
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool'
|
||||
import { PermissionRequest } from '@/components/ai-elements/permission-request'
|
||||
|
|
@ -52,20 +52,20 @@ interface ToolCall {
|
|||
timestamp: number
|
||||
}
|
||||
|
||||
interface ReasoningBlock {
|
||||
interface ErrorMessage {
|
||||
id: string
|
||||
content: string
|
||||
kind: 'error'
|
||||
message: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type ConversationItem = ChatMessage | ToolCall | ReasoningBlock
|
||||
type ConversationItem = ChatMessage | ToolCall | ErrorMessage
|
||||
|
||||
type ToolState = 'input-streaming' | 'input-available' | 'output-available' | 'output-error'
|
||||
|
||||
const isChatMessage = (item: ConversationItem): item is ChatMessage => 'role' in item
|
||||
const isToolCall = (item: ConversationItem): item is ToolCall => 'name' in item
|
||||
const isReasoningBlock = (item: ConversationItem): item is ReasoningBlock =>
|
||||
'content' in item && !('role' in item) && !('name' in item)
|
||||
const isErrorMessage = (item: ConversationItem): item is ErrorMessage => 'kind' in item && item.kind === 'error'
|
||||
|
||||
const toToolState = (status: ToolCall['status']): ToolState => {
|
||||
switch (status) {
|
||||
|
|
@ -118,7 +118,6 @@ interface ChatSidebarProps {
|
|||
onOpenFullScreen?: () => void
|
||||
conversation: ConversationItem[]
|
||||
currentAssistantMessage: string
|
||||
currentReasoning: string
|
||||
isProcessing: boolean
|
||||
isStopping?: boolean
|
||||
onStop?: () => void
|
||||
|
|
@ -145,7 +144,6 @@ export function ChatSidebar({
|
|||
onOpenFullScreen,
|
||||
conversation,
|
||||
currentAssistantMessage,
|
||||
currentReasoning,
|
||||
isProcessing,
|
||||
isStopping,
|
||||
onStop,
|
||||
|
|
@ -326,7 +324,7 @@ export function ChatSidebar({
|
|||
autoMentionRef.current = { path: selectedPath, displayName }
|
||||
}, [selectedPath, message, onMessageChange])
|
||||
|
||||
const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning
|
||||
const hasConversation = conversation.length > 0 || currentAssistantMessage
|
||||
const canSubmit = Boolean(message.trim()) && !isProcessing
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
|
@ -427,12 +425,13 @@ export function ChatSidebar({
|
|||
)
|
||||
}
|
||||
|
||||
if (isReasoningBlock(item)) {
|
||||
if (isErrorMessage(item)) {
|
||||
return (
|
||||
<Reasoning key={item.id}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{item.content}</ReasoningContent>
|
||||
</Reasoning>
|
||||
<Message key={item.id} from="assistant">
|
||||
<MessageContent className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-destructive">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs">{item.message}</pre>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -535,13 +534,6 @@ export function ChatSidebar({
|
|||
/>
|
||||
))}
|
||||
|
||||
{currentReasoning && (
|
||||
<Reasoning isStreaming>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{currentReasoning}</ReasoningContent>
|
||||
</Reasoning>
|
||||
)}
|
||||
|
||||
{currentAssistantMessage && (
|
||||
<Message from="assistant">
|
||||
<MessageContent>
|
||||
|
|
@ -550,7 +542,7 @@ export function ChatSidebar({
|
|||
</Message>
|
||||
)}
|
||||
|
||||
{isProcessing && !currentAssistantMessage && !currentReasoning && (
|
||||
{isProcessing && !currentAssistantMessage && (
|
||||
<Message from="assistant">
|
||||
<MessageContent>
|
||||
<Shimmer duration={1}>Thinking...</Shimmer>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
)
|
||||
|
||||
const activeConfig = providerConfigs[llmProvider]
|
||||
const showApiKey = llmProvider === "openai" || llmProvider === "anthropic" || llmProvider === "google" || llmProvider === "openrouter" || llmProvider === "aigateway" || llmProvider === "openai-compatible"
|
||||
const requiresApiKey = llmProvider === "openai" || llmProvider === "anthropic" || llmProvider === "google" || llmProvider === "openrouter" || llmProvider === "aigateway"
|
||||
const requiresBaseURL = llmProvider === "ollama" || llmProvider === "openai-compatible"
|
||||
const showBaseURL = llmProvider === "ollama" || llmProvider === "openai-compatible" || llmProvider === "aigateway"
|
||||
|
|
@ -690,9 +691,11 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{requiresApiKey && (
|
||||
{showApiKey && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">API Key</span>
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{llmProvider === "openai-compatible" ? "API Key (optional)" : "API Key"}
|
||||
</span>
|
||||
<Input
|
||||
type="password"
|
||||
value={activeConfig.apiKey}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
const [showMoreProviders, setShowMoreProviders] = useState(false)
|
||||
|
||||
const activeConfig = providerConfigs[provider]
|
||||
const showApiKey = provider === "openai" || provider === "anthropic" || provider === "google" || provider === "openrouter" || provider === "aigateway" || provider === "openai-compatible"
|
||||
const requiresApiKey = provider === "openai" || provider === "anthropic" || provider === "google" || provider === "openrouter" || provider === "aigateway"
|
||||
const showBaseURL = provider === "ollama" || provider === "openai-compatible" || provider === "aigateway"
|
||||
const requiresBaseURL = provider === "ollama" || provider === "openai-compatible"
|
||||
|
|
@ -398,9 +399,11 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
{requiresApiKey && (
|
||||
{showApiKey && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">API Key</span>
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{provider === "openai-compatible" ? "API Key (optional)" : "API Key"}
|
||||
</span>
|
||||
<Input
|
||||
type="password"
|
||||
value={activeConfig.apiKey}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import {
|
|||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
Copy,
|
||||
File,
|
||||
FilePlus,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
HelpCircle,
|
||||
Mic,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@
|
|||
"dev": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.44",
|
||||
"@ai-sdk/google": "^2.0.25",
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/anthropic": "^2.0.63",
|
||||
"@ai-sdk/google": "^2.0.53",
|
||||
"@ai-sdk/openai": "^2.0.91",
|
||||
"@ai-sdk/openai-compatible": "^1.0.33",
|
||||
"@ai-sdk/provider": "^2.0.1",
|
||||
"@composio/core": "^0.6.0",
|
||||
"@google-cloud/local-auth": "^3.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@x/shared": "workspace:*",
|
||||
"ai": "^5.0.102",
|
||||
"ai": "^5.0.133",
|
||||
"awilix": "^12.0.5",
|
||||
"chokidar": "^4.0.3",
|
||||
"cron-parser": "^5.5.0",
|
||||
|
|
|
|||
|
|
@ -265,6 +265,9 @@ export class StreamStepMessageBuilder {
|
|||
case "finish-step":
|
||||
this.providerOptions = event.providerOptions;
|
||||
break;
|
||||
case "error":
|
||||
this.flushBuffers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +281,30 @@ export class StreamStepMessageBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
function formatLlmStreamError(rawError: unknown): string {
|
||||
let name: string | undefined;
|
||||
let responseBody: string | undefined;
|
||||
if (rawError && typeof rawError === "object") {
|
||||
const err = rawError as Record<string, unknown>;
|
||||
const nested = (err.error && typeof err.error === "object") ? err.error as Record<string, unknown> : null;
|
||||
const nameValue = err.name ?? nested?.name;
|
||||
const responseBodyValue = err.responseBody ?? nested?.responseBody;
|
||||
if (nameValue !== undefined) {
|
||||
name = String(nameValue);
|
||||
}
|
||||
if (responseBodyValue !== undefined) {
|
||||
responseBody = String(responseBodyValue);
|
||||
}
|
||||
} else if (typeof rawError === "string") {
|
||||
responseBody = rawError;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (name) lines.push(`name: ${name}`);
|
||||
if (responseBody) lines.push(`responseBody: ${responseBody}`);
|
||||
return lines.length ? lines.join("\n") : "Model stream error";
|
||||
}
|
||||
|
||||
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||
if (id === "copilot" || id === "rowboatx") {
|
||||
return CopilotAgent;
|
||||
|
|
@ -401,6 +428,13 @@ async function buildTools(agent: z.infer<typeof Agent>): Promise<ToolSet> {
|
|||
const tools: ToolSet = {};
|
||||
for (const [name, tool] of Object.entries(agent.tools ?? {})) {
|
||||
try {
|
||||
// Skip builtin tools that declare themselves unavailable
|
||||
if (tool.type === 'builtin') {
|
||||
const builtin = BuiltinTools[tool.name];
|
||||
if (builtin?.isAvailable && !(await builtin.isAvailable())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
tools[name] = await mapAgentTool(tool);
|
||||
} catch (error) {
|
||||
console.error(`Error mapping tool ${name}:`, error);
|
||||
|
|
@ -785,6 +819,7 @@ export async function* streamAgent({
|
|||
timeZoneName: 'short'
|
||||
});
|
||||
const instructionsWithDateTime = `Current date and time: ${currentDateTime}\n\n${agent.instructions}`;
|
||||
let streamError: string | null = null;
|
||||
for await (const event of streamLlm(
|
||||
model,
|
||||
state.messages,
|
||||
|
|
@ -803,6 +838,16 @@ export async function* streamAgent({
|
|||
event: event,
|
||||
subflow: [],
|
||||
});
|
||||
if (event.type === "error") {
|
||||
streamError = event.error;
|
||||
yield* processEvent({
|
||||
runId,
|
||||
type: "error",
|
||||
error: streamError,
|
||||
subflow: [],
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// build and emit final message from agent response
|
||||
|
|
@ -815,6 +860,10 @@ export async function* streamAgent({
|
|||
subflow: [],
|
||||
});
|
||||
|
||||
if (streamError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there were any ask-human calls, emit those events
|
||||
if (message.content instanceof Array) {
|
||||
for (const part of message.content) {
|
||||
|
|
@ -888,6 +937,12 @@ async function* streamLlm(
|
|||
signal?.throwIfAborted();
|
||||
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
|
||||
switch (event.type) {
|
||||
case "error":
|
||||
yield {
|
||||
type: "error",
|
||||
error: formatLlmStreamError((event as { error?: unknown }).error ?? event),
|
||||
};
|
||||
return;
|
||||
case "reasoning-start":
|
||||
yield {
|
||||
type: "reasoning-start",
|
||||
|
|
@ -938,7 +993,7 @@ async function* streamLlm(
|
|||
};
|
||||
break;
|
||||
default:
|
||||
// console.warn("Unknown event type", event);
|
||||
console.log('unknown stream event:', JSON.stringify(event));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ When a user asks for ANY task that might require external capabilities (web sear
|
|||
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution
|
||||
- \`loadSkill\` - Skill loading
|
||||
- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.
|
||||
- \`web-search\` and \`research-search\` - Web and research search tools (available when configured). **You MUST load the \`web-search\` skill before using either of these tools.** It tells you which tool to pick and how many searches to do.
|
||||
|
||||
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import organizeFilesSkill from "./organize-files/skill.js";
|
|||
import slackSkill from "./slack/skill.js";
|
||||
import backgroundAgentsSkill from "./background-agents/skill.js";
|
||||
import createPresentationsSkill from "./create-presentations/skill.js";
|
||||
import webSearchSkill from "./web-search/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||
|
|
@ -82,6 +83,12 @@ const definitions: SkillDefinition[] = [
|
|||
summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.",
|
||||
content: mcpIntegrationSkill,
|
||||
},
|
||||
{
|
||||
id: "web-search",
|
||||
title: "Web Search",
|
||||
summary: "Searching the web or researching a topic. Guidance on when to use web-search vs research-search, and how many searches to do.",
|
||||
content: webSearchSkill,
|
||||
},
|
||||
{
|
||||
id: "deletion-guardrails",
|
||||
title: "Deletion Guardrails",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
export const skill = String.raw`
|
||||
# Web Search Skill
|
||||
|
||||
You have access to two search tools for finding information on the internet. Choose the right one based on the user's intent.
|
||||
|
||||
## Tools
|
||||
|
||||
### web-search (Brave Search)
|
||||
Quick, general-purpose web search. Returns titles, URLs, and short descriptions.
|
||||
|
||||
**Best for:**
|
||||
- Quick lookups for things that change ("current price of Bitcoin", "weather in SF")
|
||||
- Current events and breaking news
|
||||
- Finding a specific website or page
|
||||
- Simple questions with direct answers
|
||||
- Checking a fact or date
|
||||
|
||||
### research-search (Exa Search)
|
||||
Deep, research-oriented search. Returns full article text, highlights, and metadata (author, published date).
|
||||
|
||||
**Best for:**
|
||||
- Exploring a topic in depth ("what are the latest advances in CRISPR")
|
||||
- Finding articles, blog posts, papers, and quality sources
|
||||
- Discovering companies, people, or organizations
|
||||
- Research where you need rich context, not just links
|
||||
- When the user says "research", "find articles about", "look into", "deep dive"
|
||||
|
||||
**Category filter:** Use the category parameter when the user's intent clearly maps to one: company, research paper, news, tweet, personal site, financial report, people.
|
||||
|
||||
## How Many Searches to Do
|
||||
|
||||
**CRITICAL: Always start with exactly ONE search call.** Pick the single best tool (\`web-search\` or \`research-search\`) and make one request. Wait for the result before deciding if more searches are needed.
|
||||
|
||||
**NEVER call multiple search tools simultaneously.** No parallel web-search + research-search. No firing off two web-searches at once. Always sequential: one search at a time.
|
||||
|
||||
Only make a follow-up search if:
|
||||
- The first search returned truly uninformative or irrelevant results
|
||||
- The query has clearly distinct sub-topics that the first search couldn't cover (e.g., "compare X and Y" after getting results for X only)
|
||||
- The user explicitly asks you to dig deeper
|
||||
|
||||
One good search is almost always enough. Default to one and stop.
|
||||
|
||||
## Choosing Between the Two
|
||||
|
||||
If both tools are attached, prefer:
|
||||
- \`web-search\` when the user wants a quick answer or specific link
|
||||
- \`research-search\` when the user wants to learn, explore, or gather sources
|
||||
|
||||
If only one is attached, use whichever is available.
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -33,6 +33,7 @@ const BuiltinToolsSchema = z.record(z.string(), z.object({
|
|||
input: z.any(), // (input, ctx?) => Promise<any>
|
||||
output: z.promise(z.any()),
|
||||
}),
|
||||
isAvailable: z.custom<() => Promise<boolean>>().optional(),
|
||||
}));
|
||||
|
||||
type SlackToolHint = {
|
||||
|
|
@ -1265,4 +1266,215 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
return executeSlackTool("listConversations", { types: "im", limit: limit ?? 50 });
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Web Search (Brave Search API)
|
||||
// ============================================================================
|
||||
|
||||
'web-search': {
|
||||
description: 'Search the web using Brave Search. Returns web results with titles, URLs, and descriptions.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('The search query'),
|
||||
count: z.number().optional().describe('Number of results to return (default: 5, max: 20)'),
|
||||
freshness: z.string().optional().describe('Filter by freshness: pd (past day), pw (past week), pm (past month), py (past year)'),
|
||||
}),
|
||||
isAvailable: async () => {
|
||||
try {
|
||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const braveConfigPath = path.join(homedir, '.rowboat', 'config', 'brave-search.json');
|
||||
const raw = await fs.readFile(braveConfigPath, 'utf8');
|
||||
const config = JSON.parse(raw);
|
||||
return !!config.apiKey;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
execute: async ({ query, count, freshness }: { query: string; count?: number; freshness?: string }) => {
|
||||
try {
|
||||
// Read API key from config
|
||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const braveConfigPath = path.join(homedir, '.rowboat', 'config', 'brave-search.json');
|
||||
|
||||
let apiKey: string;
|
||||
try {
|
||||
const raw = await fs.readFile(braveConfigPath, 'utf8');
|
||||
const config = JSON.parse(raw);
|
||||
apiKey = config.apiKey;
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Brave Search API key not configured. Create ~/.rowboat/config/brave-search.json with { "apiKey": "<your-key>" }',
|
||||
};
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Brave Search API key is empty. Set "apiKey" in ~/.rowboat/config/brave-search.json',
|
||||
};
|
||||
}
|
||||
|
||||
// Build query params
|
||||
const resultCount = Math.min(Math.max(count || 5, 1), 20);
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
count: String(resultCount),
|
||||
});
|
||||
if (freshness) {
|
||||
params.set('freshness', freshness);
|
||||
}
|
||||
|
||||
const url = `https://api.search.brave.com/res/v1/web/search?${params.toString()}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Subscription-Token': apiKey,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `Brave Search API error (${response.status}): ${body}`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json() as {
|
||||
web?: { results?: Array<{ title?: string; url?: string; description?: string }> };
|
||||
};
|
||||
|
||||
const results = (data.web?.results || []).map((r: { title?: string; url?: string; description?: string }) => ({
|
||||
title: r.title || '',
|
||||
url: r.url || '',
|
||||
description: r.description || '',
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
query,
|
||||
results,
|
||||
count: results.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Research Search (Exa Search API)
|
||||
// ============================================================================
|
||||
|
||||
'research-search': {
|
||||
description: 'Use this for finding articles, blog posts, papers, companies, people, or exploring a topic in depth. Best for discovery and research where you need quality sources, not a quick fact.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('The search query'),
|
||||
numResults: z.number().optional().describe('Number of results to return (default: 5, max: 20)'),
|
||||
category: z.enum(['company', 'research paper', 'news', 'tweet', 'personal site', 'financial report', 'people']).optional().describe('Filter results by category'),
|
||||
}),
|
||||
isAvailable: async () => {
|
||||
try {
|
||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const exaConfigPath = path.join(homedir, '.rowboat', 'config', 'exa-search.json');
|
||||
const raw = await fs.readFile(exaConfigPath, 'utf8');
|
||||
const config = JSON.parse(raw);
|
||||
return !!config.apiKey;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
execute: async ({ query, numResults, category }: { query: string; numResults?: number; category?: string }) => {
|
||||
try {
|
||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const exaConfigPath = path.join(homedir, '.rowboat', 'config', 'exa-search.json');
|
||||
|
||||
let apiKey: string;
|
||||
try {
|
||||
const raw = await fs.readFile(exaConfigPath, 'utf8');
|
||||
const config = JSON.parse(raw);
|
||||
apiKey = config.apiKey;
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Exa Search API key not configured. Create ~/.rowboat/config/exa-search.json with { "apiKey": "<your-key>" }',
|
||||
};
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Exa Search API key is empty. Set "apiKey" in ~/.rowboat/config/exa-search.json',
|
||||
};
|
||||
}
|
||||
|
||||
const resultCount = Math.min(Math.max(numResults || 5, 1), 20);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
query,
|
||||
numResults: resultCount,
|
||||
type: 'auto',
|
||||
contents: {
|
||||
text: { maxCharacters: 1000 },
|
||||
highlights: true,
|
||||
},
|
||||
};
|
||||
if (category) {
|
||||
body.category = category;
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.exa.ai/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `Exa Search API error (${response.status}): ${text}`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json() as {
|
||||
results?: Array<{
|
||||
title?: string;
|
||||
url?: string;
|
||||
publishedDate?: string;
|
||||
author?: string;
|
||||
highlights?: string[];
|
||||
text?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const results = (data.results || []).map((r) => ({
|
||||
title: r.title || '',
|
||||
url: r.url || '',
|
||||
publishedDate: r.publishedDate || '',
|
||||
author: r.author || '',
|
||||
highlights: r.highlights || [],
|
||||
text: r.text || '',
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
query,
|
||||
results,
|
||||
count: results.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ export const LlmStepStreamFinishStepEvent = z.object({
|
|||
providerOptions: ProviderOptions.optional(),
|
||||
});
|
||||
|
||||
export const LlmStepStreamErrorEvent = BaseEvent.extend({
|
||||
type: z.literal("error"),
|
||||
error: z.string(),
|
||||
});
|
||||
|
||||
export const LlmStepStreamEvent = z.union([
|
||||
LlmStepStreamReasoningStartEvent,
|
||||
LlmStepStreamReasoningDeltaEvent,
|
||||
|
|
@ -60,4 +65,5 @@ export const LlmStepStreamEvent = z.union([
|
|||
LlmStepStreamTextEndEvent,
|
||||
LlmStepStreamToolCallEvent,
|
||||
LlmStepStreamFinishStepEvent,
|
||||
]);
|
||||
LlmStepStreamErrorEvent,
|
||||
]);
|
||||
|
|
|
|||
106
apps/x/pnpm-lock.yaml
generated
106
apps/x/pnpm-lock.yaml
generated
|
|
@ -300,19 +300,19 @@ importers:
|
|||
packages/core:
|
||||
dependencies:
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^2.0.44
|
||||
version: 2.0.57(zod@4.2.1)
|
||||
specifier: ^2.0.63
|
||||
version: 2.0.63(zod@4.2.1)
|
||||
'@ai-sdk/google':
|
||||
specifier: ^2.0.25
|
||||
version: 2.0.52(zod@4.2.1)
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^2.0.53
|
||||
version: 2.0.89(zod@4.2.1)
|
||||
version: 2.0.53(zod@4.2.1)
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^2.0.91
|
||||
version: 2.0.91(zod@4.2.1)
|
||||
'@ai-sdk/openai-compatible':
|
||||
specifier: ^1.0.27
|
||||
version: 1.0.30(zod@4.2.1)
|
||||
specifier: ^1.0.33
|
||||
version: 1.0.33(zod@4.2.1)
|
||||
'@ai-sdk/provider':
|
||||
specifier: ^2.0.0
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@composio/core':
|
||||
specifier: ^0.6.0
|
||||
|
|
@ -325,7 +325,7 @@ importers:
|
|||
version: 1.25.1(hono@4.11.3)(zod@4.2.1)
|
||||
'@openrouter/ai-sdk-provider':
|
||||
specifier: ^1.2.6
|
||||
version: 1.5.4(ai@5.0.117(zod@4.2.1))(zod@4.2.1)
|
||||
version: 1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1)
|
||||
'@react-pdf/renderer':
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.2(react@19.2.3)
|
||||
|
|
@ -336,8 +336,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
ai:
|
||||
specifier: ^5.0.102
|
||||
version: 5.0.117(zod@4.2.1)
|
||||
specifier: ^5.0.133
|
||||
version: 5.0.133(zod@4.2.1)
|
||||
awilix:
|
||||
specifier: ^12.0.5
|
||||
version: 12.0.5
|
||||
|
|
@ -405,8 +405,8 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/anthropic@2.0.57':
|
||||
resolution: {integrity: sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg==}
|
||||
'@ai-sdk/anthropic@2.0.63':
|
||||
resolution: {integrity: sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
|
@ -417,20 +417,26 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/google@2.0.52':
|
||||
resolution: {integrity: sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g==}
|
||||
'@ai-sdk/gateway@2.0.39':
|
||||
resolution: {integrity: sha512-ULnefGmRHG0/tRrf+dtDwgQYAttGi/TR0FmASAzTs1dtpeZp4Xoh1VyWrX3Z1bM3WDs9RM3ZeSE77kQT/jbfjw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.30':
|
||||
resolution: {integrity: sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w==}
|
||||
'@ai-sdk/google@2.0.53':
|
||||
resolution: {integrity: sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai@2.0.89':
|
||||
resolution: {integrity: sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw==}
|
||||
'@ai-sdk/openai-compatible@1.0.33':
|
||||
resolution: {integrity: sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai@2.0.91':
|
||||
resolution: {integrity: sha512-lozfRHfSTHg5/UliQjTDcOtISYGbEpt4FS/6QM5PcLmhdT0HmROllaBmG7+JaK+uqFtDXZGgMIpz3bqB9nzqCQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
|
@ -441,6 +447,12 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.21':
|
||||
resolution: {integrity: sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@2.0.1':
|
||||
resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3129,6 +3141,10 @@ packages:
|
|||
resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@vercel/oidc@3.1.0':
|
||||
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@vitejs/plugin-react@5.1.2':
|
||||
resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
|
@ -3245,6 +3261,12 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ai@5.0.133:
|
||||
resolution: {integrity: sha512-N6KnwSWKcXEWPnAri3anRuzRvcrvtDz1W1JG9CvMrQ0Xdp8Vu8ZToNW/eHt63CmrbmzTwVw/HaCtJuO+MYtS7A==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
|
|
@ -7065,10 +7087,10 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/anthropic@2.0.57(zod@4.2.1)':
|
||||
'@ai-sdk/anthropic@2.0.63(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.2.1)
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/gateway@2.0.24(zod@4.2.1)':
|
||||
|
|
@ -7078,22 +7100,29 @@ snapshots:
|
|||
'@vercel/oidc': 3.0.5
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/google@2.0.52(zod@4.2.1)':
|
||||
'@ai-sdk/gateway@2.0.39(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.2.1)
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
'@vercel/oidc': 3.1.0
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.30(zod@4.2.1)':
|
||||
'@ai-sdk/google@2.0.53(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.2.1)
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/openai@2.0.89(zod@4.2.1)':
|
||||
'@ai-sdk/openai-compatible@1.0.33(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.2.1)
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/openai@2.0.91(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.20(zod@4.2.1)':
|
||||
|
|
@ -7103,6 +7132,13 @@ snapshots:
|
|||
eventsource-parser: 3.0.6
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.21(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@standard-schema/spec': 1.1.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.2.1
|
||||
|
||||
'@ai-sdk/provider@2.0.1':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
|
@ -8754,10 +8790,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@octokit/openapi-types': 12.11.0
|
||||
|
||||
'@openrouter/ai-sdk-provider@1.5.4(ai@5.0.117(zod@4.2.1))(zod@4.2.1)':
|
||||
'@openrouter/ai-sdk-provider@1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@openrouter/sdk': 0.1.27
|
||||
ai: 5.0.117(zod@4.2.1)
|
||||
ai: 5.0.133(zod@4.2.1)
|
||||
zod: 4.2.1
|
||||
|
||||
'@openrouter/sdk@0.1.27':
|
||||
|
|
@ -10551,6 +10587,8 @@ snapshots:
|
|||
|
||||
'@vercel/oidc@3.0.5': {}
|
||||
|
||||
'@vercel/oidc@3.1.0': {}
|
||||
|
||||
'@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
|
|
@ -10693,6 +10731,14 @@ snapshots:
|
|||
'@opentelemetry/api': 1.9.0
|
||||
zod: 4.2.1
|
||||
|
||||
ai@5.0.133(zod@4.2.1):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.39(zod@4.2.1)
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.21(zod@4.2.1)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 4.2.1
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue