feat(copilot): add tools to access block outputs and upstream references (#2546)

* Add copilot references tools

* Minor fixes

* Omit vars field in block outputs when id is provided
This commit is contained in:
Siddharth Ganesan
2025-12-23 00:06:24 -08:00
committed by GitHub
parent c252e885af
commit 3100daa346
6 changed files with 666 additions and 0 deletions

View File

@@ -36,6 +36,8 @@ export const ToolIds = z.enum([
'manage_custom_tool',
'manage_mcp_tool',
'sleep',
'get_block_outputs',
'get_block_upstream_references',
])
export type ToolId = z.infer<typeof ToolIds>
@@ -277,6 +279,24 @@ export const ToolArgSchemas = {
.max(180)
.describe('The number of seconds to sleep (0-180, max 3 minutes)'),
}),
get_block_outputs: z.object({
blockIds: z
.array(z.string())
.optional()
.describe(
'Optional array of block UUIDs. If provided, returns outputs only for those blocks. If not provided, returns outputs for all blocks in the workflow.'
),
}),
get_block_upstream_references: z.object({
blockIds: z
.array(z.string())
.min(1)
.describe(
'Array of block UUIDs. Returns all upstream references (block outputs and variables) accessible to each block based on workflow connections.'
),
}),
} as const
export type ToolArgSchemaMap = typeof ToolArgSchemas
@@ -346,6 +366,11 @@ export const ToolSSESchemas = {
manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool),
manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool),
sleep: toolCallSSEFor('sleep', ToolArgSchemas.sleep),
get_block_outputs: toolCallSSEFor('get_block_outputs', ToolArgSchemas.get_block_outputs),
get_block_upstream_references: toolCallSSEFor(
'get_block_upstream_references',
ToolArgSchemas.get_block_upstream_references
),
} as const
export type ToolSSESchemaMap = typeof ToolSSESchemas
@@ -603,6 +628,60 @@ export const ToolResultSchemas = {
seconds: z.number(),
message: z.string().optional(),
}),
get_block_outputs: z.object({
blocks: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
outputs: z.array(z.string()),
insideSubflowOutputs: z.array(z.string()).optional(),
outsideSubflowOutputs: z.array(z.string()).optional(),
})
),
variables: z.array(
z.object({
id: z.string(),
name: z.string(),
type: z.string(),
tag: z.string(),
})
),
}),
get_block_upstream_references: z.object({
results: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
insideSubflows: z
.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
})
)
.optional(),
accessibleBlocks: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
outputs: z.array(z.string()),
accessContext: z.enum(['inside', 'outside']).optional(),
})
),
variables: z.array(
z.object({
id: z.string(),
name: z.string(),
type: z.string(),
tag: z.string(),
})
),
})
),
}),
} as const
export type ToolResultSchemaMap = typeof ToolResultSchemas

View File

@@ -0,0 +1,142 @@
import {
extractFieldsFromSchema,
parseResponseFormatSafely,
} from '@/lib/core/utils/response-format'
import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
import { getBlock } from '@/blocks'
import { useVariablesStore } from '@/stores/panel/variables/store'
import type { Variable } from '@/stores/panel/variables/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { normalizeName } from '@/stores/workflows/utils'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
export interface WorkflowContext {
workflowId: string
blocks: Record<string, BlockState>
loops: Record<string, Loop>
parallels: Record<string, Parallel>
subBlockValues: Record<string, Record<string, any>>
}
export interface VariableOutput {
id: string
name: string
type: string
tag: string
}
export function getWorkflowSubBlockValues(workflowId: string): Record<string, Record<string, any>> {
const subBlockStore = useSubBlockStore.getState()
return subBlockStore.workflowValues[workflowId] ?? {}
}
export function getMergedSubBlocks(
blocks: Record<string, BlockState>,
subBlockValues: Record<string, Record<string, any>>,
targetBlockId: string
): Record<string, any> {
const base = blocks[targetBlockId]?.subBlocks || {}
const live = subBlockValues?.[targetBlockId] || {}
const merged: Record<string, any> = { ...base }
for (const [subId, liveVal] of Object.entries(live)) {
merged[subId] = { ...(base[subId] || {}), value: liveVal }
}
return merged
}
export function getSubBlockValue(
blocks: Record<string, BlockState>,
subBlockValues: Record<string, Record<string, any>>,
targetBlockId: string,
subBlockId: string
): any {
const live = subBlockValues?.[targetBlockId]?.[subBlockId]
if (live !== undefined) return live
return blocks[targetBlockId]?.subBlocks?.[subBlockId]?.value
}
export function getWorkflowVariables(workflowId: string): VariableOutput[] {
const getVariablesByWorkflowId = useVariablesStore.getState().getVariablesByWorkflowId
const workflowVariables = getVariablesByWorkflowId(workflowId)
const validVariables = workflowVariables.filter(
(variable: Variable) => variable.name.trim() !== ''
)
return validVariables.map((variable: Variable) => ({
id: variable.id,
name: variable.name,
type: variable.type,
tag: `variable.${normalizeName(variable.name)}`,
}))
}
export function getSubflowInsidePaths(
blockType: 'loop' | 'parallel',
blockId: string,
loops: Record<string, Loop>,
parallels: Record<string, Parallel>
): string[] {
const paths = ['index']
if (blockType === 'loop') {
const loopType = loops[blockId]?.loopType || 'for'
if (loopType === 'forEach') {
paths.push('currentItem', 'items')
}
} else {
const parallelType = parallels[blockId]?.parallelType || 'count'
if (parallelType === 'collection') {
paths.push('currentItem', 'items')
}
}
return paths
}
export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext): string[] {
const { blocks, loops, parallels, subBlockValues } = ctx
const blockConfig = getBlock(block.type)
const mergedSubBlocks = getMergedSubBlocks(blocks, subBlockValues, block.id)
if (block.type === 'loop' || block.type === 'parallel') {
const insidePaths = getSubflowInsidePaths(block.type, block.id, loops, parallels)
return ['results', ...insidePaths]
}
if (block.type === 'evaluator') {
const metricsValue = getSubBlockValue(blocks, subBlockValues, block.id, 'metrics')
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name)
return validMetrics.map((metric: { name: string }) => metric.name.toLowerCase())
}
return getBlockOutputPaths(block.type, mergedSubBlocks)
}
if (block.type === 'variables') {
const variablesValue = getSubBlockValue(blocks, subBlockValues, block.id, 'variables')
if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) {
const validAssignments = variablesValue.filter((assignment: { variableName?: string }) =>
assignment?.variableName?.trim()
)
return validAssignments.map((assignment: { variableName: string }) =>
assignment.variableName.trim()
)
}
return []
}
if (blockConfig) {
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
if (responseFormat) {
const schemaFields = extractFieldsFromSchema(responseFormat)
if (schemaFields.length > 0) {
return schemaFields.map((field) => field.name)
}
}
}
return getBlockOutputPaths(block.type, mergedSubBlocks, block.triggerMode)
}
export function formatOutputsWithPrefix(paths: string[], blockName: string): string[] {
const normalizedName = normalizeName(blockName)
return paths.map((path) => `${normalizedName}.${path}`)
}

View File

@@ -0,0 +1,144 @@
import { Loader2, Tag, X, XCircle } from 'lucide-react'
import {
BaseClientTool,
type BaseClientToolMetadata,
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import {
computeBlockOutputPaths,
formatOutputsWithPrefix,
getSubflowInsidePaths,
getWorkflowSubBlockValues,
getWorkflowVariables,
} from '@/lib/copilot/tools/client/workflow/block-output-utils'
import {
GetBlockOutputsResult,
type GetBlockOutputsResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { createLogger } from '@/lib/logs/console/logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { normalizeName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('GetBlockOutputsClientTool')
interface GetBlockOutputsArgs {
blockIds?: string[]
}
export class GetBlockOutputsClientTool extends BaseClientTool {
static readonly id = 'get_block_outputs'
constructor(toolCallId: string) {
super(toolCallId, GetBlockOutputsClientTool.id, GetBlockOutputsClientTool.metadata)
}
static readonly metadata: BaseClientToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag },
[ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 },
[ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle },
[ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag },
[ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X },
[ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle },
},
getDynamicText: (params, state) => {
const blockIds = params?.blockIds
if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) {
const count = blockIds.length
switch (state) {
case ClientToolCallState.success:
return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}`
case ClientToolCallState.executing:
case ClientToolCallState.generating:
case ClientToolCallState.pending:
return `Getting outputs for ${count} block${count > 1 ? 's' : ''}`
case ClientToolCallState.error:
return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}`
}
}
return undefined
},
}
async execute(args?: GetBlockOutputsArgs): Promise<void> {
try {
this.setState(ClientToolCallState.executing)
const { activeWorkflowId } = useWorkflowRegistry.getState()
if (!activeWorkflowId) {
await this.markToolComplete(400, 'No active workflow found')
this.setState(ClientToolCallState.error)
return
}
const workflowStore = useWorkflowStore.getState()
const blocks = workflowStore.blocks || {}
const loops = workflowStore.loops || {}
const parallels = workflowStore.parallels || {}
const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId)
const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues }
const targetBlockIds =
args?.blockIds && args.blockIds.length > 0 ? args.blockIds : Object.keys(blocks)
const blockOutputs: GetBlockOutputsResultType['blocks'] = []
for (const blockId of targetBlockIds) {
const block = blocks[blockId]
if (!block?.type) continue
const blockName = block.name || block.type
const normalizedBlockName = normalizeName(blockName)
let insideSubflowOutputs: string[] | undefined
let outsideSubflowOutputs: string[] | undefined
const blockOutput: GetBlockOutputsResultType['blocks'][0] = {
blockId,
blockName,
blockType: block.type,
outputs: [],
}
if (block.type === 'loop' || block.type === 'parallel') {
const insidePaths = getSubflowInsidePaths(block.type, blockId, loops, parallels)
blockOutput.insideSubflowOutputs = formatOutputsWithPrefix(insidePaths, blockName)
blockOutput.outsideSubflowOutputs = formatOutputsWithPrefix(['results'], blockName)
} else {
const outputPaths = computeBlockOutputPaths(block, ctx)
blockOutput.outputs = formatOutputsWithPrefix(outputPaths, blockName)
}
blockOutputs.push(blockOutput)
}
const includeVariables = !args?.blockIds || args.blockIds.length === 0
const resultData: {
blocks: typeof blockOutputs
variables?: ReturnType<typeof getWorkflowVariables>
} = {
blocks: blockOutputs,
}
if (includeVariables) {
resultData.variables = getWorkflowVariables(activeWorkflowId)
}
const result = GetBlockOutputsResult.parse(resultData)
logger.info('Retrieved block outputs', {
blockCount: blockOutputs.length,
variableCount: resultData.variables?.length ?? 0,
})
await this.markToolComplete(200, 'Retrieved block outputs', result)
this.setState(ClientToolCallState.success)
} catch (error: any) {
const message = error instanceof Error ? error.message : String(error)
logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message })
await this.markToolComplete(500, message || 'Failed to get block outputs')
this.setState(ClientToolCallState.error)
}
}
}

View File

@@ -0,0 +1,227 @@
import { GitBranch, Loader2, X, XCircle } from 'lucide-react'
import {
BaseClientTool,
type BaseClientToolMetadata,
ClientToolCallState,
} from '@/lib/copilot/tools/client/base-tool'
import {
computeBlockOutputPaths,
formatOutputsWithPrefix,
getSubflowInsidePaths,
getWorkflowSubBlockValues,
getWorkflowVariables,
} from '@/lib/copilot/tools/client/workflow/block-output-utils'
import {
GetBlockUpstreamReferencesResult,
type GetBlockUpstreamReferencesResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { createLogger } from '@/lib/logs/console/logger'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
const logger = createLogger('GetBlockUpstreamReferencesClientTool')
interface GetBlockUpstreamReferencesArgs {
blockIds: string[]
}
export class GetBlockUpstreamReferencesClientTool extends BaseClientTool {
static readonly id = 'get_block_upstream_references'
constructor(toolCallId: string) {
super(
toolCallId,
GetBlockUpstreamReferencesClientTool.id,
GetBlockUpstreamReferencesClientTool.metadata
)
}
static readonly metadata: BaseClientToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch },
[ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 },
[ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle },
[ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch },
[ClientToolCallState.error]: { text: 'Failed to get references', icon: X },
[ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle },
},
getDynamicText: (params, state) => {
const blockIds = params?.blockIds
if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) {
const count = blockIds.length
switch (state) {
case ClientToolCallState.success:
return `Retrieved references for ${count} block${count > 1 ? 's' : ''}`
case ClientToolCallState.executing:
case ClientToolCallState.generating:
case ClientToolCallState.pending:
return `Getting references for ${count} block${count > 1 ? 's' : ''}`
case ClientToolCallState.error:
return `Failed to get references for ${count} block${count > 1 ? 's' : ''}`
}
}
return undefined
},
}
async execute(args?: GetBlockUpstreamReferencesArgs): Promise<void> {
try {
this.setState(ClientToolCallState.executing)
if (!args?.blockIds || args.blockIds.length === 0) {
await this.markToolComplete(400, 'blockIds array is required')
this.setState(ClientToolCallState.error)
return
}
const { activeWorkflowId } = useWorkflowRegistry.getState()
if (!activeWorkflowId) {
await this.markToolComplete(400, 'No active workflow found')
this.setState(ClientToolCallState.error)
return
}
const workflowStore = useWorkflowStore.getState()
const blocks = workflowStore.blocks || {}
const edges = workflowStore.edges || []
const loops = workflowStore.loops || {}
const parallels = workflowStore.parallels || {}
const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId)
const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues }
const variableOutputs = getWorkflowVariables(activeWorkflowId)
const graphEdges = edges.map((edge) => ({ source: edge.source, target: edge.target }))
const results: GetBlockUpstreamReferencesResultType['results'] = []
for (const blockId of args.blockIds) {
const targetBlock = blocks[blockId]
if (!targetBlock) {
logger.warn(`Block ${blockId} not found`)
continue
}
const insideSubflows: { blockId: string; blockName: string; blockType: string }[] = []
const containingLoopIds = new Set<string>()
const containingParallelIds = new Set<string>()
Object.values(loops as Record<string, Loop>).forEach((loop) => {
if (loop?.nodes?.includes(blockId)) {
containingLoopIds.add(loop.id)
const loopBlock = blocks[loop.id]
if (loopBlock) {
insideSubflows.push({
blockId: loop.id,
blockName: loopBlock.name || loopBlock.type,
blockType: 'loop',
})
}
}
})
Object.values(parallels as Record<string, Parallel>).forEach((parallel) => {
if (parallel?.nodes?.includes(blockId)) {
containingParallelIds.add(parallel.id)
const parallelBlock = blocks[parallel.id]
if (parallelBlock) {
insideSubflows.push({
blockId: parallel.id,
blockName: parallelBlock.name || parallelBlock.type,
blockType: 'parallel',
})
}
}
})
const ancestorIds = BlockPathCalculator.findAllPathNodes(graphEdges, blockId)
const accessibleIds = new Set<string>(ancestorIds)
accessibleIds.add(blockId)
const starterBlock = Object.values(blocks).find(
(b) => b.type === 'starter' || b.type === 'start_trigger'
)
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
accessibleIds.add(starterBlock.id)
}
containingLoopIds.forEach((loopId) => {
accessibleIds.add(loopId)
loops[loopId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId))
})
containingParallelIds.forEach((parallelId) => {
accessibleIds.add(parallelId)
parallels[parallelId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId))
})
const accessibleBlocks: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'] =
[]
for (const accessibleBlockId of accessibleIds) {
const block = blocks[accessibleBlockId]
if (!block?.type) continue
const canSelfReference = block.type === 'approval' || block.type === 'human_in_the_loop'
if (accessibleBlockId === blockId && !canSelfReference) continue
const blockName = block.name || block.type
let accessContext: 'inside' | 'outside' | undefined
let outputPaths: string[]
if (block.type === 'loop' || block.type === 'parallel') {
const isInside =
(block.type === 'loop' && containingLoopIds.has(accessibleBlockId)) ||
(block.type === 'parallel' && containingParallelIds.has(accessibleBlockId))
accessContext = isInside ? 'inside' : 'outside'
outputPaths = isInside
? getSubflowInsidePaths(block.type, accessibleBlockId, loops, parallels)
: ['results']
} else {
outputPaths = computeBlockOutputPaths(block, ctx)
}
const formattedOutputs = formatOutputsWithPrefix(outputPaths, blockName)
const entry: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'][0] = {
blockId: accessibleBlockId,
blockName,
blockType: block.type,
outputs: formattedOutputs,
}
if (accessContext) entry.accessContext = accessContext
accessibleBlocks.push(entry)
}
const resultEntry: GetBlockUpstreamReferencesResultType['results'][0] = {
blockId,
blockName: targetBlock.name || targetBlock.type,
accessibleBlocks,
variables: variableOutputs,
}
if (insideSubflows.length > 0) resultEntry.insideSubflows = insideSubflows
results.push(resultEntry)
}
const result = GetBlockUpstreamReferencesResult.parse({ results })
logger.info('Retrieved upstream references', {
blockIds: args.blockIds,
resultCount: results.length,
})
await this.markToolComplete(200, 'Retrieved upstream references', result)
this.setState(ClientToolCallState.success)
} catch (error: any) {
const message = error instanceof Error ? error.message : String(error)
logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message })
await this.markToolComplete(500, message || 'Failed to get upstream references')
this.setState(ClientToolCallState.error)
}
}
}

View File

@@ -104,3 +104,71 @@ export const KnowledgeBaseResultSchema = z.object({
data: z.any().optional(),
})
export type KnowledgeBaseResult = z.infer<typeof KnowledgeBaseResultSchema>
export const GetBlockOutputsInput = z.object({
blockIds: z.array(z.string()).optional(),
})
export const GetBlockOutputsResult = z.object({
blocks: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
outputs: z.array(z.string()),
insideSubflowOutputs: z.array(z.string()).optional(),
outsideSubflowOutputs: z.array(z.string()).optional(),
})
),
variables: z
.array(
z.object({
id: z.string(),
name: z.string(),
type: z.string(),
tag: z.string(),
})
)
.optional(),
})
export type GetBlockOutputsInputType = z.infer<typeof GetBlockOutputsInput>
export type GetBlockOutputsResultType = z.infer<typeof GetBlockOutputsResult>
export const GetBlockUpstreamReferencesInput = z.object({
blockIds: z.array(z.string()).min(1),
})
export const GetBlockUpstreamReferencesResult = z.object({
results: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
insideSubflows: z
.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
})
)
.optional(),
accessibleBlocks: z.array(
z.object({
blockId: z.string(),
blockName: z.string(),
blockType: z.string(),
outputs: z.array(z.string()),
accessContext: z.enum(['inside', 'outside']).optional(),
})
),
variables: z.array(
z.object({
id: z.string(),
name: z.string(),
type: z.string(),
tag: z.string(),
})
),
})
),
})
export type GetBlockUpstreamReferencesInputType = z.infer<typeof GetBlockUpstreamReferencesInput>
export type GetBlockUpstreamReferencesResultType = z.infer<typeof GetBlockUpstreamReferencesResult>

View File

@@ -41,6 +41,8 @@ import { SetEnvironmentVariablesClientTool } from '@/lib/copilot/tools/client/us
import { CheckDeploymentStatusClientTool } from '@/lib/copilot/tools/client/workflow/check-deployment-status'
import { DeployWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/deploy-workflow'
import { EditWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/edit-workflow'
import { GetBlockOutputsClientTool } from '@/lib/copilot/tools/client/workflow/get-block-outputs'
import { GetBlockUpstreamReferencesClientTool } from '@/lib/copilot/tools/client/workflow/get-block-upstream-references'
import { GetUserWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/get-user-workflow'
import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console'
import { GetWorkflowDataClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-data'
@@ -110,6 +112,8 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
manage_custom_tool: (id) => new ManageCustomToolClientTool(id),
manage_mcp_tool: (id) => new ManageMcpToolClientTool(id),
sleep: (id) => new SleepClientTool(id),
get_block_outputs: (id) => new GetBlockOutputsClientTool(id),
get_block_upstream_references: (id) => new GetBlockUpstreamReferencesClientTool(id),
}
// Read-only static metadata for class-based tools (no instances)
@@ -150,6 +154,8 @@ export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefi
manage_custom_tool: (ManageCustomToolClientTool as any)?.metadata,
manage_mcp_tool: (ManageMcpToolClientTool as any)?.metadata,
sleep: (SleepClientTool as any)?.metadata,
get_block_outputs: (GetBlockOutputsClientTool as any)?.metadata,
get_block_upstream_references: (GetBlockUpstreamReferencesClientTool as any)?.metadata,
}
function ensureClientToolInstance(toolName: string | undefined, toolCallId: string | undefined) {