feat: rendering for agents
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import {
|
||||
AbstractNodeProps,
|
||||
createAbstractNodeComponent,
|
||||
useAbstractNode,
|
||||
} from '@/lib/graph/abstractNode'
|
||||
import { getConfigContent } from '@/lib/graph/configTypes'
|
||||
import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
@@ -18,50 +17,24 @@ import { InputHandle, OutputHandle } from '@/components/graph/NodeHandles'
|
||||
import FlowContext from '@/lib/graph/flowContext'
|
||||
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
||||
import { getNodeType } from '@/lib/graph/nodeRegistry'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||
import { Bot, Play, Loader2 } from 'lucide-react'
|
||||
import { Bot } from 'lucide-react'
|
||||
|
||||
export type AgentNodeData = {
|
||||
/** Additional context for the agent (configuration). */
|
||||
context?: string
|
||||
/** Cached output from last run; set by connected Rendering node. Not displayed on this node. */
|
||||
outputMarkdown?: string
|
||||
error?: string
|
||||
loading?: boolean
|
||||
/** Signature of inputs (sourceIds + config contents) from the last successful run. Used to show on-hold when current inputs differ. */
|
||||
/** Signature of inputs from last successful run; set by Rendering node when it runs the agent. Used as cache. */
|
||||
lastRunSourceSignature?: string
|
||||
}
|
||||
|
||||
type Props = AbstractNodeProps<AgentNodeData>
|
||||
|
||||
function serializeNodeForContext(nodes: { id: string; type?: string; data?: unknown }[], nodeId: string): string {
|
||||
const node = nodes.find((n: { id: string }) => n.id === nodeId)
|
||||
if (!node) return `${nodeId}: (not found)`
|
||||
const type = node.type ?? 'unknown'
|
||||
const data = node.data as Record<string, unknown> | undefined
|
||||
if (type === 'config') {
|
||||
const content = getConfigContent(data)
|
||||
return `[config ${nodeId}]\n${content || '(empty)'}`
|
||||
}
|
||||
if (type === 'variable') {
|
||||
const v = data?.value
|
||||
return `[variable ${nodeId}]: ${v === undefined || v === null ? '' : String(v)}`
|
||||
}
|
||||
if (type === 'data') {
|
||||
const rows = (data?.rows as Record<string, string>[] | undefined) ?? []
|
||||
const columns = (data?.columns as string[] | undefined) ?? []
|
||||
const preview = rows.slice(0, 20).map((r) => columns.map((c) => r[c] ?? '').join(', ')).join('\n')
|
||||
return `[data ${nodeId}] ${columns.length} columns, ${rows.length} rows\n${preview}${rows.length > 20 ? '\n...' : ''}`
|
||||
}
|
||||
return `[${type} ${nodeId}]: ${JSON.stringify(data ?? {}).slice(0, 200)}`
|
||||
}
|
||||
|
||||
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const flowContext = useContext(FlowContext)
|
||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
||||
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
||||
const { aiConnection } = usePlatform()
|
||||
const [running, setRunning] = useState(false)
|
||||
|
||||
const dimensions =
|
||||
width != null && height != null && width > 0 && height > 0
|
||||
@@ -69,9 +42,6 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
: undefined
|
||||
|
||||
const contextText = data?.context ?? ''
|
||||
const outputMarkdown = data?.outputMarkdown
|
||||
const error = data?.error
|
||||
const loading = data?.loading ?? false
|
||||
|
||||
const connectedSources = useMemo(() => {
|
||||
return sourceIds.map((sid) => {
|
||||
@@ -80,84 +50,17 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
})
|
||||
}, [sourceIds, nodes])
|
||||
|
||||
const sourceSignature = useMemo(() => {
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
const n = nodes.find((n: { id: string }) => n.id === sid)
|
||||
return (n as { type?: string } | undefined)?.type === 'config'
|
||||
})
|
||||
.map((sid) => getConfigContent((nodes.find((n: { id: string }) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined))
|
||||
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
|
||||
}, [sourceIds, nodes])
|
||||
|
||||
const runAgent = useCallback(async () => {
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
const n = nodes.find((n: { id: string }) => n.id === sid)
|
||||
return (n as { type?: string } | undefined)?.type === 'config'
|
||||
})
|
||||
.map((sid) => getConfigContent((nodes.find((n: { id: string }) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined))
|
||||
const prompt = configContents.length > 0 ? configContents.join('\n\n---\n\n') : 'No prompt provided. Please describe what you want in structured markdown.'
|
||||
const contextNodes = sourceIds.map((sid) => ({ id: sid, content: serializeNodeForContext(nodes, sid) }))
|
||||
|
||||
updateData({ error: undefined, loading: true })
|
||||
setRunning(true)
|
||||
try {
|
||||
const res = await fetch('/api/agent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
context: contextText.trim() || undefined,
|
||||
contextNodes,
|
||||
connection: aiConnection,
|
||||
}),
|
||||
})
|
||||
const json = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
updateData({
|
||||
loading: false,
|
||||
error: (json as { error?: string }).error ?? `Request failed: ${res.status}`,
|
||||
outputMarkdown: undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
const markdown = (json as { markdown?: string }).markdown ?? ''
|
||||
updateData({
|
||||
loading: false,
|
||||
error: undefined,
|
||||
outputMarkdown: markdown,
|
||||
lastRunSourceSignature: sourceSignature,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
updateData({
|
||||
loading: false,
|
||||
error: err instanceof Error ? err.message : 'Agent request failed',
|
||||
outputMarkdown: undefined,
|
||||
})
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}, [sourceIds, nodes, contextText, sourceSignature, updateData, aiConnection])
|
||||
|
||||
const onContextChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => updateData({ context: e.target.value }),
|
||||
[updateData]
|
||||
)
|
||||
|
||||
const pathNodeIds = flowContext?.connectionPathNodeIds
|
||||
const triggerNodeIds = flowContext?.connectionPathTriggerNodeIds ?? []
|
||||
const lastRunSourceSignature = data?.lastRunSourceSignature
|
||||
const hasPendingInputs =
|
||||
pathNodeIds?.has(id) &&
|
||||
triggerNodeIds.length > 0 &&
|
||||
!loading &&
|
||||
sourceSignature !== lastRunSourceSignature
|
||||
|
||||
// Agent is configuration-only; it does not block the flow. Only the Rendering node reports
|
||||
// paused/updating, so the connection path runs trigger → … → agent (on-path) → rendering (paused).
|
||||
useSyncConnectionStatus(id, {
|
||||
updating: running || loading,
|
||||
error: !!error,
|
||||
paused: hasPendingInputs && !running && !loading,
|
||||
updating: false,
|
||||
error: false,
|
||||
paused: false,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -177,23 +80,6 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
<BaseNodeHeaderRow
|
||||
icon={<Bot className="size-4" />}
|
||||
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||
right={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 h-7 nodrag nopan"
|
||||
onClick={runAgent}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running || loading ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-3.5" />
|
||||
)}
|
||||
<span className="ml-1.5">Run</span>
|
||||
</Button>
|
||||
}
|
||||
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||
/>
|
||||
<BaseNodeContent>
|
||||
@@ -224,19 +110,14 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-xs text-destructive">{error}</p>
|
||||
)}
|
||||
{outputMarkdown != null && outputMarkdown !== '' && !error && (
|
||||
<div className="text-xs text-muted-foreground border rounded p-2 max-h-24 overflow-auto">
|
||||
<span className="font-medium">Output:</span> {outputMarkdown.length} chars (connect to Renderer to view)
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect to a Renderer node and click Run there to generate output.
|
||||
</p>
|
||||
</div>
|
||||
</BaseNodeContent>
|
||||
<BaseNodeFooter>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="agent">
|
||||
{outputMarkdown != null ? `${outputMarkdown.length} chars` : error ? 'Error' : '—'}
|
||||
—
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
|
||||
@@ -1,15 +1,131 @@
|
||||
/**
|
||||
* Agent node rendering logic: provides the agent's markdown output to the Rendering node.
|
||||
* Update mode is manual (user clicks Run on the renderer).
|
||||
* Agent node rendering logic: when the Rendering node runs, it calls the agent API
|
||||
* and updates the agent node's output. Update mode is manual (user clicks Run on the renderer).
|
||||
*/
|
||||
|
||||
import type { SourceRenderingLogic } from '@/lib/graph/sourceRenderingLogic'
|
||||
import { getConfigContent } from '@/lib/graph/configTypes'
|
||||
import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/graph/sourceRenderingLogic'
|
||||
import type { AiConnection } from '@/app/kosmos/KosmosContext'
|
||||
|
||||
export const agentRenderingLogic: SourceRenderingLogic = {
|
||||
defaultUpdateMode: 'manual',
|
||||
getResolvedContent: async (context) => {
|
||||
const sourceNode = context.nodes.find((n) => n.id === context.sourceNodeId)
|
||||
const outputMarkdown = (sourceNode?.data as { outputMarkdown?: string } | undefined)?.outputMarkdown ?? ''
|
||||
function serializeNodeForContext(
|
||||
nodes: { id: string; type?: string; data?: unknown }[],
|
||||
nodeId: string
|
||||
): string {
|
||||
const node = nodes.find((n) => n.id === nodeId)
|
||||
if (!node) return `${nodeId}: (not found)`
|
||||
const type = node.type ?? 'unknown'
|
||||
const data = node.data as Record<string, unknown> | undefined
|
||||
if (type === 'config') {
|
||||
const content = getConfigContent(data)
|
||||
return `[config ${nodeId}]\n${content || '(empty)'}`
|
||||
}
|
||||
if (type === 'variable') {
|
||||
const v = data?.value
|
||||
return `[variable ${nodeId}]: ${v === undefined || v === null ? '' : String(v)}`
|
||||
}
|
||||
if (type === 'data') {
|
||||
const rows = (data?.rows as Record<string, string>[] | undefined) ?? []
|
||||
const columns = (data?.columns as string[] | undefined) ?? []
|
||||
const preview = rows
|
||||
.slice(0, 20)
|
||||
.map((r) => columns.map((c) => r[c] ?? '').join(', '))
|
||||
.join('\n')
|
||||
return `[data ${nodeId}] ${columns.length} columns, ${rows.length} rows\n${preview}${rows.length > 20 ? '\n...' : ''}`
|
||||
}
|
||||
return `[${type} ${nodeId}]: ${JSON.stringify(data ?? {}).slice(0, 200)}`
|
||||
}
|
||||
|
||||
function buildSourceSignature(
|
||||
nodes: { id: string; type?: string; data?: unknown }[],
|
||||
sourceIds: string[]
|
||||
): string {
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
const n = nodes.find((nn) => nn.id === sid)
|
||||
return (n as { type?: string } | undefined)?.type === 'config'
|
||||
})
|
||||
.map((sid) =>
|
||||
getConfigContent(
|
||||
(nodes.find((n) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined
|
||||
)
|
||||
)
|
||||
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
|
||||
}
|
||||
|
||||
export const agentRenderingLogic = {
|
||||
defaultUpdateMode: 'manual' as const,
|
||||
getResolvedContent: async (context: SourceRenderingLogicContext): Promise<ResolvedContentResult> => {
|
||||
const { nodes, sourceNodeId, setNodes, aiConnection } = context
|
||||
const sourceNode = nodes.find((n) => n.id === sourceNodeId)
|
||||
const agentData = sourceNode?.data as { context?: string; outputMarkdown?: string } | undefined
|
||||
const contextText = agentData?.context ?? ''
|
||||
const sourceIds = (context.edges
|
||||
.filter((e) => e.target === sourceNodeId)
|
||||
.map((e) => e.source)) as string[]
|
||||
|
||||
type NodeWithData = { id: string; type?: string; data?: unknown }
|
||||
const setAgentData = (patch: Record<string, unknown>) => {
|
||||
if (!setNodes) return
|
||||
setNodes((prev: unknown[]) =>
|
||||
(prev as NodeWithData[]).map((n) =>
|
||||
n.id === sourceNodeId ? { ...n, data: { ...(n.data as object), ...patch } } : n
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (setNodes && aiConnection) {
|
||||
const connection = aiConnection as AiConnection
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
const n = nodes.find((nn) => nn.id === sid)
|
||||
return (n as { type?: string } | undefined)?.type === 'config'
|
||||
})
|
||||
.map((sid) =>
|
||||
getConfigContent(
|
||||
(nodes.find((n) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined
|
||||
)
|
||||
)
|
||||
const prompt =
|
||||
configContents.length > 0
|
||||
? configContents.join('\n\n---\n\n')
|
||||
: 'No prompt provided. Please describe what you want in structured markdown.'
|
||||
const contextNodes = sourceIds.map((sid) => ({
|
||||
id: sid,
|
||||
content: serializeNodeForContext(nodes, sid),
|
||||
}))
|
||||
const sourceSignature = buildSourceSignature(nodes, sourceIds)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/agent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
context: contextText.trim() || undefined,
|
||||
contextNodes,
|
||||
connection,
|
||||
}),
|
||||
})
|
||||
const json = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
setAgentData({ outputMarkdown: undefined })
|
||||
const msg = (json as { error?: string }).error ?? `Request failed: ${res.status}`
|
||||
throw new Error(msg)
|
||||
}
|
||||
const markdown = (json as { markdown?: string }).markdown ?? ''
|
||||
setAgentData({
|
||||
outputMarkdown: markdown,
|
||||
lastRunSourceSignature: sourceSignature,
|
||||
})
|
||||
return { resolved: markdown, outputTypeId: 'markdown' }
|
||||
} catch (err: unknown) {
|
||||
setAgentData({ outputMarkdown: undefined })
|
||||
throw err instanceof Error ? err : new Error('Agent request failed')
|
||||
}
|
||||
}
|
||||
|
||||
const outputMarkdown =
|
||||
(sourceNode?.data as { outputMarkdown?: string } | undefined)?.outputMarkdown ?? ''
|
||||
return { resolved: outputMarkdown, outputTypeId: 'markdown' }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useAbstractNode,
|
||||
} from '@/lib/graph/abstractNode'
|
||||
import { getConfigContent, getConfigType, getConfigTypeId } from '@/lib/graph/configTypes'
|
||||
import { getSourceRenderingLogic } from '@/lib/graph/sourceRenderingLogic'
|
||||
import { getSourceRenderingLogic, type SourceRenderingLogicContext } from '@/lib/graph/sourceRenderingLogic'
|
||||
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
|
||||
import { useResizeHeight } from '@/hooks/useResizeHeight'
|
||||
import { plantumlLanguage } from '@/lib/plantumlLanguage'
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTheme } from '@/lib/themeContext'
|
||||
import { toast } from 'sonner'
|
||||
@@ -79,6 +80,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const lastManualRunTriggerRef = useRef<number>(0)
|
||||
const { nodes, edges, setNodes, setEdges, sourceIds, updateData } = useAbstractNode<RenderingNodeData>(id, data ?? {})
|
||||
const { aiConnection } = usePlatform()
|
||||
const viewportWidth = data?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH
|
||||
const viewportHeight = data?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT
|
||||
|
||||
@@ -265,7 +267,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
setResolvedContent(null)
|
||||
setError({
|
||||
kind: 'no-content',
|
||||
message: isAgentSource ? 'Run the Agent node to generate output, then click Run here.' : 'Click Run to render.',
|
||||
message: 'Click Run to render.',
|
||||
})
|
||||
setLoading(false)
|
||||
return
|
||||
@@ -289,11 +291,13 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const context = {
|
||||
nodes,
|
||||
edges,
|
||||
sourceNodeId: srcId,
|
||||
sourceNodeId: srcId!,
|
||||
renderNodeId: id,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
}
|
||||
setNodes: setNodes ?? undefined,
|
||||
aiConnection,
|
||||
} as SourceRenderingLogicContext
|
||||
const { resolved, outputTypeId } = await logic.getResolvedContent(context)
|
||||
if (cancelled || thisRunId !== runIdRef.current) return
|
||||
setResolvedContent(resolved)
|
||||
@@ -351,7 +355,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
}, [id, srcId, srcNode?.type, effectiveUpdateMode, runTrigger, sourceContent, sourceSignature, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, nodes, edges, updateData])
|
||||
}, [id, srcId, srcNode?.type, effectiveUpdateMode, runTrigger, sourceContent, sourceSignature, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, nodes, edges, updateData, setNodes, aiConnection])
|
||||
|
||||
|
||||
const dimensions =
|
||||
@@ -579,9 +583,9 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
>
|
||||
Raw
|
||||
</MenubarCheckboxItem>
|
||||
<MenubarSeparator />
|
||||
{outputType === 'image' && (
|
||||
<MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarSubTrigger className="text-xs">Viewport size</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[12rem] p-2">
|
||||
<div className="grid gap-2">
|
||||
|
||||
@@ -40,7 +40,7 @@ export type FlowContextValue = {
|
||||
connectionPathPausedSegmentNodeIds: Set<string>
|
||||
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
||||
connectionPathActiveSegmentNodeIds: Set<string>
|
||||
/** Archon-type nodes that are on hold (e.g. Agent waiting for Run). */
|
||||
/** Nodes that are on hold (e.g. Renderer in manual mode waiting for Run). */
|
||||
connectionPathPausedNodeIds: string[]
|
||||
/** Add this node as paused (on hold); remove when user continues. */
|
||||
addConnectionPathPausedNode: (nodeId: string) => void
|
||||
|
||||
@@ -92,7 +92,7 @@ export function getPathNodeIds(
|
||||
}
|
||||
|
||||
/**
|
||||
* Path nodes from triggers up to and including the first paused node (Archon on hold).
|
||||
* Path nodes from triggers up to and including the first paused node (e.g. Renderer waiting for Run).
|
||||
* Used to color those edges yellow; rest of path stays blue.
|
||||
*/
|
||||
export function getPausedSegmentNodeIds(
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
||||
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
||||
* path is computed from triggers + updating nodes.
|
||||
* - **Updating** – Node is doing async work (e.g. agent running, renderer loading).
|
||||
* - **Updating** – Node is doing async work (e.g. renderer loading, agent run triggered by renderer).
|
||||
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
||||
* edges on the path show "updating" (blue).
|
||||
* - **Paused** – Node is on hold (e.g. agent waiting for Run after inputs changed).
|
||||
* - **Paused** – Node is on hold (e.g. renderer in manual mode waiting for Run after inputs changed).
|
||||
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
||||
* segment show "paused" (yellow).
|
||||
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
||||
|
||||
@@ -16,6 +16,10 @@ export type SourceRenderingLogicContext = {
|
||||
renderNodeId: string
|
||||
viewportWidth?: number
|
||||
viewportHeight?: number
|
||||
/** Optional: allows source logic to update other nodes (e.g. agent run updates agent node). */
|
||||
setNodes?: (updater: (nodes: { id: string; type?: string; data?: unknown }[]) => { id: string; type?: string; data?: unknown }[]) => void
|
||||
/** Optional: AI connection for agent source (used when Rendering node runs the agent). */
|
||||
aiConnection?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user