mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Add subagents
This commit is contained in:
committed by
Emir Karabeg
parent
fd76e98f0e
commit
df80309c3b
@@ -4,74 +4,21 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronUp } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* Max height for thinking content before internal scrolling kicks in
|
||||
*/
|
||||
const THINKING_MAX_HEIGHT = 125
|
||||
|
||||
/**
|
||||
* Interval for auto-scroll during streaming (ms)
|
||||
*/
|
||||
const SCROLL_INTERVAL = 100
|
||||
|
||||
/**
|
||||
* Timer update interval in milliseconds
|
||||
*/
|
||||
const TIMER_UPDATE_INTERVAL = 100
|
||||
|
||||
/**
|
||||
* Milliseconds threshold for displaying as seconds
|
||||
*/
|
||||
const SECONDS_THRESHOLD = 1000
|
||||
|
||||
/**
|
||||
* Props for the ShimmerOverlayText component
|
||||
*/
|
||||
interface ShimmerOverlayTextProps {
|
||||
/** Label text to display */
|
||||
label: string
|
||||
/** Value text to display */
|
||||
value: string
|
||||
/** Whether the shimmer animation is active */
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* ShimmerOverlayText component for thinking block
|
||||
* Applies shimmer effect to the "Thought for X.Xs" text during streaming
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns Text with optional shimmer overlay effect
|
||||
*/
|
||||
function ShimmerOverlayText({ label, value, active = false }: ShimmerOverlayTextProps) {
|
||||
return (
|
||||
<span className='relative inline-block'>
|
||||
<span className='text-[var(--text-tertiary)]'>{label}</span>
|
||||
<span className='text-[var(--text-muted)]'>{value}</span>
|
||||
{active ? (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 select-none overflow-hidden'
|
||||
>
|
||||
<span
|
||||
className='block text-transparent'
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
animation: 'thinking-shimmer 1.4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{value}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
<style>{`
|
||||
@keyframes thinking-shimmer {
|
||||
0% { background-position: 150% 0; }
|
||||
50% { background-position: 0% 0; }
|
||||
100% { background-position: -150% 0; }
|
||||
}
|
||||
`}</style>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the ThinkingBlock component
|
||||
*/
|
||||
@@ -80,16 +27,15 @@ interface ThinkingBlockProps {
|
||||
content: string
|
||||
/** Whether the block is currently streaming */
|
||||
isStreaming?: boolean
|
||||
/** Persisted duration from content block */
|
||||
duration?: number
|
||||
/** Persisted start time from content block */
|
||||
startTime?: number
|
||||
/** Whether there are more content blocks after this one (e.g., tool calls) */
|
||||
hasFollowingContent?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* ThinkingBlock component displays AI reasoning/thinking process
|
||||
* Shows collapsible content with duration timer
|
||||
* Auto-expands during streaming and collapses when complete
|
||||
* Auto-collapses when a tool call or other content comes in after it
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns Thinking block with expandable content and timer
|
||||
@@ -97,29 +43,21 @@ interface ThinkingBlockProps {
|
||||
export function ThinkingBlock({
|
||||
content,
|
||||
isStreaming = false,
|
||||
duration: persistedDuration,
|
||||
startTime: persistedStartTime,
|
||||
hasFollowingContent = false,
|
||||
}: ThinkingBlockProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [duration, setDuration] = useState(persistedDuration ?? 0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const userCollapsedRef = useRef<boolean>(false)
|
||||
const startTimeRef = useRef<number>(persistedStartTime ?? Date.now())
|
||||
|
||||
/**
|
||||
* Updates start time reference when persisted start time changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (typeof persistedStartTime === 'number') {
|
||||
startTimeRef.current = persistedStartTime
|
||||
}
|
||||
}, [persistedStartTime])
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const startTimeRef = useRef<number>(Date.now())
|
||||
|
||||
/**
|
||||
* Auto-expands block when streaming with content
|
||||
* Auto-collapses when streaming ends
|
||||
* Auto-collapses when streaming ends OR when following content arrives
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isStreaming) {
|
||||
// Collapse if streaming ended or if there's following content (like a tool call)
|
||||
if (!isStreaming || hasFollowingContent) {
|
||||
setIsExpanded(false)
|
||||
userCollapsedRef.current = false
|
||||
return
|
||||
@@ -128,42 +66,57 @@ export function ThinkingBlock({
|
||||
if (!userCollapsedRef.current && content && content.trim().length > 0) {
|
||||
setIsExpanded(true)
|
||||
}
|
||||
}, [isStreaming, content])
|
||||
}, [isStreaming, content, hasFollowingContent])
|
||||
|
||||
/**
|
||||
* Updates duration timer during streaming
|
||||
* Uses persisted duration when available
|
||||
*/
|
||||
// Reset start time when streaming begins
|
||||
useEffect(() => {
|
||||
if (typeof persistedDuration === 'number') {
|
||||
setDuration(persistedDuration)
|
||||
return
|
||||
if (isStreaming && !hasFollowingContent) {
|
||||
startTimeRef.current = Date.now()
|
||||
setDuration(0)
|
||||
}
|
||||
}, [isStreaming, hasFollowingContent])
|
||||
|
||||
if (isStreaming) {
|
||||
const interval = setInterval(() => {
|
||||
setDuration(Date.now() - startTimeRef.current)
|
||||
}, TIMER_UPDATE_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
// Update duration timer during streaming (stop when following content arrives)
|
||||
useEffect(() => {
|
||||
// Stop timer if not streaming or if there's following content (thinking is done)
|
||||
if (!isStreaming || hasFollowingContent) return
|
||||
|
||||
setDuration(Date.now() - startTimeRef.current)
|
||||
}, [isStreaming, persistedDuration])
|
||||
const interval = setInterval(() => {
|
||||
setDuration(Date.now() - startTimeRef.current)
|
||||
}, TIMER_UPDATE_INTERVAL)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isStreaming, hasFollowingContent])
|
||||
|
||||
// Auto-scroll to bottom during streaming using interval (same as copilot chat)
|
||||
useEffect(() => {
|
||||
if (!isStreaming || !isExpanded) return
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}, SCROLL_INTERVAL)
|
||||
|
||||
return () => window.clearInterval(intervalId)
|
||||
}, [isStreaming, isExpanded])
|
||||
|
||||
/**
|
||||
* Formats duration in milliseconds to human-readable format
|
||||
* @param ms - Duration in milliseconds
|
||||
* @returns Formatted string (e.g., "150ms" or "2.5s")
|
||||
* Formats duration in milliseconds to seconds
|
||||
* Always shows seconds, rounded to nearest whole second, minimum 1s
|
||||
*/
|
||||
const formatDuration = (ms: number) => {
|
||||
if (ms < SECONDS_THRESHOLD) {
|
||||
return `${ms}ms`
|
||||
}
|
||||
const seconds = (ms / SECONDS_THRESHOLD).toFixed(1)
|
||||
const seconds = Math.max(1, Math.round(ms / 1000))
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
const hasContent = content && content.trim().length > 0
|
||||
const label = isStreaming ? 'Thinking' : 'Thought'
|
||||
const durationText = ` for ${formatDuration(duration)}`
|
||||
|
||||
return (
|
||||
<div className='mt-1 mb-0'>
|
||||
@@ -180,21 +133,54 @@ export function ThinkingBlock({
|
||||
type='button'
|
||||
disabled={!hasContent}
|
||||
>
|
||||
<ShimmerOverlayText
|
||||
label='Thought'
|
||||
value={` for ${formatDuration(duration)}`}
|
||||
active={isStreaming}
|
||||
/>
|
||||
<span className='relative inline-block'>
|
||||
<span className='text-[var(--text-tertiary)]'>{label}</span>
|
||||
<span className='text-[var(--text-muted)]'>{durationText}</span>
|
||||
{isStreaming && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 select-none overflow-hidden'
|
||||
>
|
||||
<span
|
||||
className='block text-transparent'
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
animation: 'thinking-shimmer 1.4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{durationText}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<style>{`
|
||||
@keyframes thinking-shimmer {
|
||||
0% { background-position: 150% 0; }
|
||||
50% { background-position: 0% 0; }
|
||||
100% { background-position: -150% 0; }
|
||||
}
|
||||
`}</style>
|
||||
</span>
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx('h-3 w-3 transition-transform', isExpanded && 'rotate-180')}
|
||||
className={clsx('h-3 w-3 transition-transform', isExpanded ? 'rotate-180' : 'rotate-90')}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className='ml-1 border-[var(--border-1)] border-l-2 pl-2'>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className='ml-1 overflow-y-auto border-[var(--border-1)] border-l-2 pl-2'
|
||||
style={{ maxHeight: THINKING_MAX_HEIGHT }}
|
||||
>
|
||||
<pre className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'>
|
||||
{content}
|
||||
{isStreaming && (
|
||||
|
||||
@@ -201,19 +201,14 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
)
|
||||
}
|
||||
if (block.type === 'thinking') {
|
||||
const isLastBlock = index === message.contentBlocks!.length - 1
|
||||
// Consider the thinking block streaming if the overall message is streaming
|
||||
// and the block has not been finalized with a duration yet. This avoids
|
||||
// freezing the timer when new blocks are appended after the thinking block.
|
||||
const isStreamingThinking = isStreaming && (block as any).duration == null
|
||||
|
||||
// Check if there are any blocks after this one (tool calls, text, etc.)
|
||||
const hasFollowingContent = index < message.contentBlocks!.length - 1
|
||||
return (
|
||||
<div key={`thinking-${index}-${block.timestamp || index}`} className='w-full'>
|
||||
<ThinkingBlock
|
||||
content={block.content}
|
||||
isStreaming={isStreamingThinking}
|
||||
duration={block.duration}
|
||||
startTime={block.startTime}
|
||||
isStreaming={isStreaming}
|
||||
hasFollowingContent={hasFollowingContent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronUp } from 'lucide-react'
|
||||
import { Button, Code } from '@/components/emcn'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
|
||||
import { getClientTool } from '@/lib/copilot/tools/client/manager'
|
||||
import { getRegisteredTools } from '@/lib/copilot/tools/client/registry'
|
||||
import { CLASS_TOOL_METADATA, useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
import type { CopilotToolCall } from '@/stores/panel/copilot/types'
|
||||
import type { CopilotToolCall, SubAgentContentBlock } from '@/stores/panel/copilot/types'
|
||||
|
||||
interface ToolCallProps {
|
||||
toolCall?: CopilotToolCall
|
||||
@@ -226,6 +228,213 @@ function ShimmerOverlayText({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SubAgentToolCall renders a nested tool call from a subagent in a muted/thinking style.
|
||||
*/
|
||||
function SubAgentToolCall({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
const displayName = getDisplayNameForSubAgent(toolCall)
|
||||
|
||||
const isLoading =
|
||||
toolCall.state === ClientToolCallState.pending ||
|
||||
toolCall.state === ClientToolCallState.executing
|
||||
|
||||
return (
|
||||
<div className='py-0.5'>
|
||||
<span className='relative inline-block font-[470] font-season text-[12px] text-[var(--text-tertiary)]'>
|
||||
{displayName}
|
||||
{isLoading && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 select-none overflow-hidden'
|
||||
>
|
||||
<span
|
||||
className='block text-transparent'
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
animation: 'subagent-shimmer 1.4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen',
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<style>{`
|
||||
@keyframes subagent-shimmer {
|
||||
0% { background-position: 150% 0; }
|
||||
50% { background-position: 0% 0; }
|
||||
100% { background-position: -150% 0; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for subagent tool calls
|
||||
*/
|
||||
function getDisplayNameForSubAgent(toolCall: CopilotToolCall): string {
|
||||
const fromStore = toolCall.display?.text
|
||||
if (fromStore) return fromStore
|
||||
|
||||
const stateVerb = getStateVerb(toolCall.state)
|
||||
const formattedName = formatToolName(toolCall.name)
|
||||
return `${stateVerb} ${formattedName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Max height for subagent content before internal scrolling kicks in
|
||||
*/
|
||||
const SUBAGENT_MAX_HEIGHT = 125
|
||||
|
||||
/**
|
||||
* Interval for auto-scroll during streaming (ms)
|
||||
*/
|
||||
const SUBAGENT_SCROLL_INTERVAL = 100
|
||||
|
||||
/**
|
||||
* SubAgentContent renders the streamed content and tool calls from a subagent
|
||||
* with thinking-style styling (same as ThinkingBlock).
|
||||
* Auto-collapses when streaming ends and has internal scrolling for long content.
|
||||
*/
|
||||
function SubAgentContent({
|
||||
blocks,
|
||||
isStreaming = false,
|
||||
}: {
|
||||
blocks?: SubAgentContentBlock[]
|
||||
isStreaming?: boolean
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const userCollapsedRef = useRef<boolean>(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Auto-expand when streaming with content, auto-collapse when done
|
||||
useEffect(() => {
|
||||
if (!isStreaming) {
|
||||
setIsExpanded(false)
|
||||
userCollapsedRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!userCollapsedRef.current && blocks && blocks.length > 0) {
|
||||
setIsExpanded(true)
|
||||
}
|
||||
}, [isStreaming, blocks])
|
||||
|
||||
// Auto-scroll to bottom during streaming using interval (same as copilot chat)
|
||||
useEffect(() => {
|
||||
if (!isStreaming || !isExpanded) return
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}, SUBAGENT_SCROLL_INTERVAL)
|
||||
|
||||
return () => window.clearInterval(intervalId)
|
||||
}, [isStreaming, isExpanded])
|
||||
|
||||
if (!blocks || blocks.length === 0) return null
|
||||
|
||||
const hasContent = blocks.length > 0
|
||||
const label = isStreaming ? 'Debugging' : 'Debugged'
|
||||
|
||||
return (
|
||||
<div className='mt-1 mb-0'>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsExpanded((v) => {
|
||||
const next = !v
|
||||
if (!next && isStreaming) userCollapsedRef.current = true
|
||||
return next
|
||||
})
|
||||
}}
|
||||
className='mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
disabled={!hasContent}
|
||||
>
|
||||
<span className='relative inline-block'>
|
||||
<span className='text-[var(--text-tertiary)]'>{label}</span>
|
||||
{isStreaming && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 select-none overflow-hidden'
|
||||
>
|
||||
<span
|
||||
className='block text-transparent'
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
animation: 'thinking-shimmer 1.4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx('h-3 w-3 transition-transform', isExpanded ? 'rotate-180' : 'rotate-90')}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className='ml-1 overflow-y-auto border-[var(--border-1)] border-l-2 pl-2'
|
||||
style={{ maxHeight: SUBAGENT_MAX_HEIGHT }}
|
||||
>
|
||||
{blocks.map((block, index) => {
|
||||
if (block.type === 'subagent_text' && block.content) {
|
||||
const isLastBlock = index === blocks.length - 1
|
||||
return (
|
||||
<pre
|
||||
key={`subagent-text-${index}`}
|
||||
className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'
|
||||
>
|
||||
{block.content}
|
||||
{isStreaming && isLastBlock && (
|
||||
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-tertiary)]' />
|
||||
)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
if (block.type === 'subagent_tool_call' && block.toolCall) {
|
||||
return (
|
||||
<SubAgentToolCall
|
||||
key={`subagent-tool-${block.toolCall.id || index}`}
|
||||
toolCall={block.toolCall}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a tool call is "special" and should display with gradient styling.
|
||||
* Only workflow operation tools (edit, build, run, deploy) get the purple gradient.
|
||||
@@ -559,6 +768,18 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
// Skip rendering some internal tools
|
||||
if (toolCall.name === 'checkoff_todo' || toolCall.name === 'mark_todo_in_progress') return null
|
||||
|
||||
// Special rendering for debug tool with subagent content - only show the collapsible SubAgentContent
|
||||
if (toolCall.name === 'debug' && toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0) {
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<SubAgentContent
|
||||
blocks={toolCall.subAgentBlocks}
|
||||
isStreaming={toolCall.subAgentStreaming}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Get current mode from store to determine if we should render integration tools
|
||||
const mode = useCopilotStore.getState().mode
|
||||
|
||||
@@ -862,6 +1083,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
const safeInputs = inputs && typeof inputs === 'object' ? inputs : {}
|
||||
const inputEntries = Object.entries(safeInputs)
|
||||
|
||||
// Don't show the table if there are no inputs
|
||||
if (inputEntries.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
|
||||
<table className='w-full table-fixed bg-transparent'>
|
||||
@@ -876,14 +1102,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='bg-transparent'>
|
||||
{inputEntries.length === 0 ? (
|
||||
<tr className='border-[var(--border-1)] border-t bg-transparent'>
|
||||
<td colSpan={2} className='px-[10px] py-[8px] text-[var(--text-muted)] text-xs'>
|
||||
No inputs provided
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
inputEntries.map(([key, value]) => (
|
||||
{inputEntries.map(([key, value]) => (
|
||||
<tr
|
||||
key={key}
|
||||
className='group relative border-[var(--border-1)] border-t bg-transparent'
|
||||
@@ -932,8 +1151,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -986,6 +1204,13 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
editedParams={editedParams}
|
||||
/>
|
||||
)}
|
||||
{/* Render subagent content */}
|
||||
{toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0 && (
|
||||
<SubAgentContent
|
||||
blocks={toolCall.subAgentBlocks}
|
||||
isStreaming={toolCall.subAgentStreaming}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1041,6 +1266,13 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
editedParams={editedParams}
|
||||
/>
|
||||
)}
|
||||
{/* Render subagent content */}
|
||||
{toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0 && (
|
||||
<SubAgentContent
|
||||
blocks={toolCall.subAgentBlocks}
|
||||
isStreaming={toolCall.subAgentStreaming}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1143,6 +1375,13 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Render subagent content (from debug tool or other subagents) */}
|
||||
{toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0 && (
|
||||
<SubAgentContent
|
||||
blocks={toolCall.subAgentBlocks}
|
||||
isStreaming={toolCall.subAgentStreaming}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
49
apps/sim/lib/copilot/tools/client/other/debug.ts
Normal file
49
apps/sim/lib/copilot/tools/client/other/debug.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Bug, Loader2, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
|
||||
interface DebugArgs {
|
||||
error_description: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug tool that spawns a subagent to diagnose workflow issues.
|
||||
* This tool auto-executes and the actual work is done by the debug subagent.
|
||||
* The subagent's output is streamed as nested content under this tool call.
|
||||
*/
|
||||
export class DebugClientTool extends BaseClientTool {
|
||||
static readonly id = 'debug'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, DebugClientTool.id, DebugClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Preparing debug session', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 },
|
||||
[ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Debugged', icon: Bug },
|
||||
[ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle },
|
||||
[ClientToolCallState.rejected]: { text: 'Debug skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Debug aborted', icon: XCircle },
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the debug tool.
|
||||
* This just marks the tool as executing - the actual debug work is done server-side
|
||||
* by the debug subagent, and its output is streamed as subagent events.
|
||||
*/
|
||||
async execute(_args?: DebugArgs): Promise<void> {
|
||||
// Immediately transition to executing state - no user confirmation needed
|
||||
this.setState(ClientToolCallState.executing)
|
||||
// The tool result will come from the server via tool_result event
|
||||
// when the debug subagent completes its work
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { PlanClientTool } from '@/lib/copilot/tools/client/other/plan'
|
||||
import { RememberDebugClientTool } from '@/lib/copilot/tools/client/other/remember-debug'
|
||||
import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation'
|
||||
import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors'
|
||||
import { DebugClientTool } from '@/lib/copilot/tools/client/other/debug'
|
||||
import { SearchOnlineClientTool } from '@/lib/copilot/tools/client/other/search-online'
|
||||
import { SearchPatternsClientTool } from '@/lib/copilot/tools/client/other/search-patterns'
|
||||
import { SleepClientTool } from '@/lib/copilot/tools/client/other/sleep'
|
||||
@@ -78,6 +79,7 @@ try {
|
||||
|
||||
// Known class-based client tools: map tool name -> instantiator
|
||||
const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
|
||||
debug: (id) => new DebugClientTool(id),
|
||||
run_workflow: (id) => new RunWorkflowClientTool(id),
|
||||
get_workflow_console: (id) => new GetWorkflowConsoleClientTool(id),
|
||||
get_blocks_and_tools: (id) => new GetBlocksAndToolsClientTool(id),
|
||||
@@ -120,6 +122,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
|
||||
|
||||
// Read-only static metadata for class-based tools (no instances)
|
||||
export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefined> = {
|
||||
debug: (DebugClientTool as any)?.metadata,
|
||||
run_workflow: (RunWorkflowClientTool as any)?.metadata,
|
||||
get_workflow_console: (GetWorkflowConsoleClientTool as any)?.metadata,
|
||||
get_blocks_and_tools: (GetBlocksAndToolsClientTool as any)?.metadata,
|
||||
@@ -650,6 +653,14 @@ interface StreamingContext {
|
||||
newChatId?: string
|
||||
doneEventCount: number
|
||||
streamComplete?: boolean
|
||||
/** Track active subagent sessions by parent tool call ID */
|
||||
subAgentParentToolCallId?: string
|
||||
/** Track subagent content per parent tool call */
|
||||
subAgentContent: Record<string, string>
|
||||
/** Track subagent tool calls per parent tool call */
|
||||
subAgentToolCalls: Record<string, CopilotToolCall[]>
|
||||
/** Track subagent streaming blocks per parent tool call */
|
||||
subAgentBlocks: Record<string, any[]>
|
||||
}
|
||||
|
||||
type SSEHandler = (
|
||||
@@ -1474,6 +1485,304 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
default: () => {},
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to update a tool call with subagent data in both toolCallsById and contentBlocks
|
||||
*/
|
||||
function updateToolCallWithSubAgentData(
|
||||
context: StreamingContext,
|
||||
get: () => CopilotStore,
|
||||
set: any,
|
||||
parentToolCallId: string
|
||||
) {
|
||||
const { toolCallsById } = get()
|
||||
const parentToolCall = toolCallsById[parentToolCallId]
|
||||
if (!parentToolCall) {
|
||||
logger.warn('[SubAgent] updateToolCallWithSubAgentData: parent tool call not found', {
|
||||
parentToolCallId,
|
||||
availableToolCallIds: Object.keys(toolCallsById),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare subagent blocks array for ordered display
|
||||
const blocks = context.subAgentBlocks[parentToolCallId] || []
|
||||
|
||||
const updatedToolCall: CopilotToolCall = {
|
||||
...parentToolCall,
|
||||
subAgentContent: context.subAgentContent[parentToolCallId] || '',
|
||||
subAgentToolCalls: context.subAgentToolCalls[parentToolCallId] || [],
|
||||
subAgentBlocks: blocks,
|
||||
subAgentStreaming: true,
|
||||
}
|
||||
|
||||
logger.info('[SubAgent] Updating tool call with subagent data', {
|
||||
parentToolCallId,
|
||||
parentToolName: parentToolCall.name,
|
||||
subAgentContentLength: updatedToolCall.subAgentContent?.length,
|
||||
subAgentBlocksCount: updatedToolCall.subAgentBlocks?.length,
|
||||
subAgentToolCallsCount: updatedToolCall.subAgentToolCalls?.length,
|
||||
})
|
||||
|
||||
// Update in toolCallsById
|
||||
const updatedMap = { ...toolCallsById, [parentToolCallId]: updatedToolCall }
|
||||
set({ toolCallsById: updatedMap })
|
||||
|
||||
// Update in contentBlocks
|
||||
let foundInContentBlocks = false
|
||||
for (let i = 0; i < context.contentBlocks.length; i++) {
|
||||
const b = context.contentBlocks[i] as any
|
||||
if (b.type === 'tool_call' && b.toolCall?.id === parentToolCallId) {
|
||||
context.contentBlocks[i] = { ...b, toolCall: updatedToolCall }
|
||||
foundInContentBlocks = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundInContentBlocks) {
|
||||
logger.warn('[SubAgent] Parent tool call not found in contentBlocks', {
|
||||
parentToolCallId,
|
||||
contentBlocksCount: context.contentBlocks.length,
|
||||
toolCallBlockIds: context.contentBlocks
|
||||
.filter((b: any) => b.type === 'tool_call')
|
||||
.map((b: any) => b.toolCall?.id),
|
||||
})
|
||||
}
|
||||
|
||||
updateStreamingMessage(set, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE handlers for subagent events (events with subagent field set)
|
||||
* These handle content and tool calls from subagents like debug
|
||||
*/
|
||||
const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
// Handle subagent response start (ignore - just a marker)
|
||||
start: () => {
|
||||
// Subagent start event - no action needed, parent is already tracked from subagent_start
|
||||
},
|
||||
|
||||
// Handle subagent text content (reasoning/thinking)
|
||||
content: (data, context, get, set) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
logger.info('[SubAgent] content event', {
|
||||
parentToolCallId,
|
||||
hasData: !!data.data,
|
||||
dataPreview: typeof data.data === 'string' ? data.data.substring(0, 50) : null,
|
||||
})
|
||||
if (!parentToolCallId || !data.data) {
|
||||
logger.warn('[SubAgent] content missing parentToolCallId or data', {
|
||||
parentToolCallId,
|
||||
hasData: !!data.data,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize if needed
|
||||
if (!context.subAgentContent[parentToolCallId]) {
|
||||
context.subAgentContent[parentToolCallId] = ''
|
||||
}
|
||||
if (!context.subAgentBlocks[parentToolCallId]) {
|
||||
context.subAgentBlocks[parentToolCallId] = []
|
||||
}
|
||||
|
||||
// Append content
|
||||
context.subAgentContent[parentToolCallId] += data.data
|
||||
|
||||
// Update or create the last text block in subAgentBlocks
|
||||
const blocks = context.subAgentBlocks[parentToolCallId]
|
||||
const lastBlock = blocks[blocks.length - 1]
|
||||
if (lastBlock && lastBlock.type === 'subagent_text') {
|
||||
lastBlock.content = (lastBlock.content || '') + data.data
|
||||
} else {
|
||||
blocks.push({
|
||||
type: 'subagent_text',
|
||||
content: data.data,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
updateToolCallWithSubAgentData(context, get, set, parentToolCallId)
|
||||
},
|
||||
|
||||
// Handle subagent reasoning (same as content for subagent display purposes)
|
||||
reasoning: (data, context, get, set) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
const phase = data?.phase || data?.data?.phase
|
||||
if (!parentToolCallId) return
|
||||
|
||||
// Initialize if needed
|
||||
if (!context.subAgentContent[parentToolCallId]) {
|
||||
context.subAgentContent[parentToolCallId] = ''
|
||||
}
|
||||
if (!context.subAgentBlocks[parentToolCallId]) {
|
||||
context.subAgentBlocks[parentToolCallId] = []
|
||||
}
|
||||
|
||||
// For reasoning, we just append the content (treating start/end as markers)
|
||||
if (phase === 'start' || phase === 'end') return
|
||||
|
||||
const chunk = typeof data?.data === 'string' ? data.data : data?.content || ''
|
||||
if (!chunk) return
|
||||
|
||||
context.subAgentContent[parentToolCallId] += chunk
|
||||
|
||||
// Update or create the last text block in subAgentBlocks
|
||||
const blocks = context.subAgentBlocks[parentToolCallId]
|
||||
const lastBlock = blocks[blocks.length - 1]
|
||||
if (lastBlock && lastBlock.type === 'subagent_text') {
|
||||
lastBlock.content = (lastBlock.content || '') + chunk
|
||||
} else {
|
||||
blocks.push({
|
||||
type: 'subagent_text',
|
||||
content: chunk,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
updateToolCallWithSubAgentData(context, get, set, parentToolCallId)
|
||||
},
|
||||
|
||||
// Handle subagent tool_generating (tool is being generated)
|
||||
tool_generating: () => {
|
||||
// Tool generating event - no action needed, we'll handle the actual tool_call
|
||||
},
|
||||
|
||||
// Handle subagent tool calls - also execute client tools
|
||||
tool_call: async (data, context, get, set) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (!parentToolCallId) return
|
||||
|
||||
const toolData = data?.data || {}
|
||||
const id: string | undefined = toolData.id || data?.toolCallId
|
||||
const name: string | undefined = toolData.name || data?.toolName
|
||||
if (!id || !name) return
|
||||
|
||||
const args = toolData.arguments
|
||||
|
||||
// Initialize if needed
|
||||
if (!context.subAgentToolCalls[parentToolCallId]) {
|
||||
context.subAgentToolCalls[parentToolCallId] = []
|
||||
}
|
||||
if (!context.subAgentBlocks[parentToolCallId]) {
|
||||
context.subAgentBlocks[parentToolCallId] = []
|
||||
}
|
||||
|
||||
// Ensure client tool instance is registered (for execution)
|
||||
ensureClientToolInstance(name, id)
|
||||
|
||||
// Create or update the subagent tool call
|
||||
const existingIndex = context.subAgentToolCalls[parentToolCallId].findIndex((tc) => tc.id === id)
|
||||
const subAgentToolCall: CopilotToolCall = {
|
||||
id,
|
||||
name,
|
||||
state: ClientToolCallState.pending,
|
||||
...(args ? { params: args } : {}),
|
||||
display: resolveToolDisplay(name, ClientToolCallState.pending, id, args),
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
context.subAgentToolCalls[parentToolCallId][existingIndex] = subAgentToolCall
|
||||
} else {
|
||||
context.subAgentToolCalls[parentToolCallId].push(subAgentToolCall)
|
||||
|
||||
// Also add to ordered blocks
|
||||
context.subAgentBlocks[parentToolCallId].push({
|
||||
type: 'subagent_tool_call',
|
||||
toolCall: subAgentToolCall,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Also add to main toolCallsById for proper tool execution
|
||||
const { toolCallsById } = get()
|
||||
const updated = { ...toolCallsById, [id]: subAgentToolCall }
|
||||
set({ toolCallsById: updated })
|
||||
|
||||
updateToolCallWithSubAgentData(context, get, set, parentToolCallId)
|
||||
|
||||
// Execute client tools (same logic as main tool_call handler)
|
||||
try {
|
||||
const def = getTool(name)
|
||||
if (def) {
|
||||
const hasInterrupt =
|
||||
typeof def.hasInterrupt === 'function' ? !!def.hasInterrupt(args || {}) : !!def.hasInterrupt
|
||||
if (!hasInterrupt) {
|
||||
// Auto-execute tools without interrupts
|
||||
const ctx = createExecutionContext({ toolCallId: id, toolName: name })
|
||||
try {
|
||||
await def.execute(args || {}, ctx)
|
||||
} catch (execErr: any) {
|
||||
logger.error('[SubAgent] Tool execution failed', { id, name, error: execErr?.message })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to class-based tools
|
||||
const instance = getClientTool(id)
|
||||
if (instance) {
|
||||
const hasInterruptDisplays = !!instance.getInterruptDisplays?.()
|
||||
if (!hasInterruptDisplays) {
|
||||
try {
|
||||
await instance.execute(args)
|
||||
} catch (execErr: any) {
|
||||
logger.error('[SubAgent] Class tool execution failed', { id, name, error: execErr?.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('[SubAgent] Tool registry/execution error', { id, name, error: e?.message })
|
||||
}
|
||||
},
|
||||
|
||||
// Handle subagent tool results
|
||||
tool_result: (data, context, get, set) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (!parentToolCallId) return
|
||||
|
||||
const toolCallId: string | undefined = data?.toolCallId || data?.data?.id
|
||||
const success: boolean | undefined = data?.success !== false // Default to true if not specified
|
||||
if (!toolCallId) return
|
||||
|
||||
// Initialize if needed
|
||||
if (!context.subAgentToolCalls[parentToolCallId]) return
|
||||
if (!context.subAgentBlocks[parentToolCallId]) return
|
||||
|
||||
// Update the subagent tool call state
|
||||
const targetState = success ? ClientToolCallState.success : ClientToolCallState.error
|
||||
const existingIndex = context.subAgentToolCalls[parentToolCallId].findIndex(
|
||||
(tc) => tc.id === toolCallId
|
||||
)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const existing = context.subAgentToolCalls[parentToolCallId][existingIndex]
|
||||
context.subAgentToolCalls[parentToolCallId][existingIndex] = {
|
||||
...existing,
|
||||
state: targetState,
|
||||
display: resolveToolDisplay(existing.name, targetState, toolCallId, existing.params),
|
||||
}
|
||||
|
||||
// Also update in ordered blocks
|
||||
for (const block of context.subAgentBlocks[parentToolCallId]) {
|
||||
if (block.type === 'subagent_tool_call' && block.toolCall?.id === toolCallId) {
|
||||
block.toolCall = context.subAgentToolCalls[parentToolCallId][existingIndex]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateToolCallWithSubAgentData(context, get, set, parentToolCallId)
|
||||
},
|
||||
|
||||
// Handle subagent stream done - just update the streaming state
|
||||
done: (data, context, get, set) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (!parentToolCallId) return
|
||||
|
||||
// Update the tool call with final content but keep streaming true until subagent_end
|
||||
updateToolCallWithSubAgentData(context, get, set, parentToolCallId)
|
||||
},
|
||||
}
|
||||
|
||||
// Debounced UI update queue for smoother streaming
|
||||
const streamingUpdateQueue = new Map<string, StreamingContext>()
|
||||
let streamingUpdateRAF: number | null = null
|
||||
@@ -2540,6 +2849,9 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
designWorkflowContent: '',
|
||||
pendingContent: '',
|
||||
doneEventCount: 0,
|
||||
subAgentContent: {},
|
||||
subAgentToolCalls: {},
|
||||
subAgentBlocks: {},
|
||||
}
|
||||
|
||||
if (isContinuation) {
|
||||
@@ -2563,6 +2875,99 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
const { abortController } = get()
|
||||
if (abortController?.signal.aborted) break
|
||||
|
||||
// Log SSE events for debugging
|
||||
logger.info('[SSE] Received event', {
|
||||
type: data.type,
|
||||
hasSubAgent: !!data.subagent,
|
||||
subagent: data.subagent,
|
||||
dataPreview:
|
||||
typeof data.data === 'string'
|
||||
? data.data.substring(0, 100)
|
||||
: JSON.stringify(data.data)?.substring(0, 100),
|
||||
})
|
||||
|
||||
// Handle subagent_start to track parent tool call
|
||||
if (data.type === 'subagent_start') {
|
||||
const toolCallId = data.data?.tool_call_id
|
||||
if (toolCallId) {
|
||||
context.subAgentParentToolCallId = toolCallId
|
||||
// Mark the parent tool call as streaming
|
||||
const { toolCallsById } = get()
|
||||
const parentToolCall = toolCallsById[toolCallId]
|
||||
if (parentToolCall) {
|
||||
const updatedToolCall: CopilotToolCall = {
|
||||
...parentToolCall,
|
||||
subAgentStreaming: true,
|
||||
}
|
||||
const updatedMap = { ...toolCallsById, [toolCallId]: updatedToolCall }
|
||||
set({ toolCallsById: updatedMap })
|
||||
}
|
||||
logger.info('[SSE] Subagent session started', {
|
||||
subagent: data.subagent,
|
||||
parentToolCallId: toolCallId,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle subagent_end to finalize subagent content
|
||||
if (data.type === 'subagent_end') {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (parentToolCallId) {
|
||||
// Mark subagent streaming as complete
|
||||
const { toolCallsById } = get()
|
||||
const parentToolCall = toolCallsById[parentToolCallId]
|
||||
if (parentToolCall) {
|
||||
const updatedToolCall: CopilotToolCall = {
|
||||
...parentToolCall,
|
||||
subAgentContent: context.subAgentContent[parentToolCallId] || '',
|
||||
subAgentToolCalls: context.subAgentToolCalls[parentToolCallId] || [],
|
||||
subAgentBlocks: context.subAgentBlocks[parentToolCallId] || [],
|
||||
subAgentStreaming: false, // Done streaming
|
||||
}
|
||||
const updatedMap = { ...toolCallsById, [parentToolCallId]: updatedToolCall }
|
||||
set({ toolCallsById: updatedMap })
|
||||
logger.info('[SSE] Subagent session ended', {
|
||||
subagent: data.subagent,
|
||||
parentToolCallId,
|
||||
contentLength: context.subAgentContent[parentToolCallId]?.length || 0,
|
||||
toolCallCount: context.subAgentToolCalls[parentToolCallId]?.length || 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
context.subAgentParentToolCallId = undefined
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a subagent event (has subagent field)
|
||||
if (data.subagent) {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (!parentToolCallId) {
|
||||
logger.warn('[SSE] Subagent event without parent tool call ID', {
|
||||
type: data.type,
|
||||
subagent: data.subagent,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.info('[SSE] Processing subagent event', {
|
||||
type: data.type,
|
||||
subagent: data.subagent,
|
||||
parentToolCallId,
|
||||
hasHandler: !!subAgentSSEHandlers[data.type],
|
||||
})
|
||||
|
||||
const subAgentHandler = subAgentSSEHandlers[data.type]
|
||||
if (subAgentHandler) {
|
||||
await subAgentHandler(data, context, get, set)
|
||||
} else {
|
||||
logger.warn('[SSE] No handler for subagent event type', { type: data.type })
|
||||
}
|
||||
// Skip regular handlers for subagent events
|
||||
if (context.streamComplete) break
|
||||
continue
|
||||
}
|
||||
|
||||
const handler = sseHandlers[data.type] || sseHandlers.default
|
||||
await handler(data, context, get, set)
|
||||
if (context.streamComplete) break
|
||||
|
||||
@@ -2,12 +2,30 @@ import type { ClientToolCallState, ClientToolDisplay } from '@/lib/copilot/tools
|
||||
|
||||
export type ToolState = ClientToolCallState
|
||||
|
||||
/**
|
||||
* Subagent content block for nested thinking/reasoning inside a tool call
|
||||
*/
|
||||
export interface SubAgentContentBlock {
|
||||
type: 'subagent_text' | 'subagent_tool_call'
|
||||
content?: string
|
||||
toolCall?: CopilotToolCall
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface CopilotToolCall {
|
||||
id: string
|
||||
name: string
|
||||
state: ClientToolCallState
|
||||
params?: Record<string, any>
|
||||
display?: ClientToolDisplay
|
||||
/** Content streamed from a subagent (e.g., debug agent) */
|
||||
subAgentContent?: string
|
||||
/** Tool calls made by the subagent */
|
||||
subAgentToolCalls?: CopilotToolCall[]
|
||||
/** Structured content blocks for subagent (thinking + tool calls in order) */
|
||||
subAgentBlocks?: SubAgentContentBlock[]
|
||||
/** Whether subagent is currently streaming */
|
||||
subAgentStreaming?: boolean
|
||||
}
|
||||
|
||||
export interface MessageFileAttachment {
|
||||
|
||||
Reference in New Issue
Block a user