chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,193 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
const { isKnownTool, isSimExecuted, isClientExecuted } = vi.hoisted(() => ({
isKnownTool: vi.fn(),
isSimExecuted: vi.fn(),
isClientExecuted: vi.fn(),
}))
const { executeAppTool } = vi.hoisted(() => ({
executeAppTool: vi.fn(),
}))
vi.mock('./router', () => ({
isKnownTool,
isSimExecuted,
isClientExecuted,
}))
vi.mock('@/tools', () => ({
executeTool: executeAppTool,
}))
import { clearHandlers, executeTool, registerHandler } from './executor'
describe('copilot tool executor fallback', () => {
beforeEach(() => {
vi.clearAllMocks()
clearHandlers()
})
it('falls back to app tool executor for dynamic sim tools', async () => {
isKnownTool.mockReturnValue(false)
isSimExecuted.mockReturnValue(false)
executeAppTool.mockResolvedValue({ success: true, output: { emails: [] } })
const result = await executeTool(
'gmail_read',
{ maxResults: 10, credentialId: 'cred-123' },
{ userId: 'user-1', workflowId: 'workflow-1', workspaceId: 'ws-1', chatId: 'chat-1' }
)
expect(executeAppTool).toHaveBeenCalledWith(
'gmail_read',
expect.objectContaining({
maxResults: 10,
credentialId: 'cred-123',
credential: 'cred-123',
_context: expect.objectContaining({
userId: 'user-1',
workflowId: 'workflow-1',
workspaceId: 'ws-1',
chatId: 'chat-1',
enforceCredentialAccess: true,
}),
})
)
expect(result).toEqual({ success: true, output: { emails: [] } })
})
it('uses the registered handler for client-routed tools when running headless (Mothership block)', async () => {
isKnownTool.mockReturnValue(true)
isSimExecuted.mockReturnValue(false)
isClientExecuted.mockReturnValue(true)
const runWorkflowHandler = vi.fn().mockResolvedValue({ success: true, output: { ran: true } })
registerHandler('run_workflow', runWorkflowHandler)
const context = { userId: 'user-1', workflowId: 'workflow-1', workspaceId: 'ws-1' }
const result = await executeTool('run_workflow', { workflow_input: {} }, context)
expect(runWorkflowHandler).toHaveBeenCalledWith({ workflow_input: {} }, context)
expect(executeAppTool).not.toHaveBeenCalled()
expect(result).toEqual({ success: true, output: { ran: true } })
})
it('falls back to app tool executor for client-routed tools with no registered handler', async () => {
isKnownTool.mockReturnValue(true)
isSimExecuted.mockReturnValue(false)
isClientExecuted.mockReturnValue(true)
executeAppTool.mockResolvedValue({
success: false,
error: 'Tool not found: unknown_client_tool',
})
await executeTool('unknown_client_tool', {}, { userId: 'user-1' })
expect(executeAppTool).toHaveBeenCalledWith('unknown_client_tool', expect.any(Object))
})
it('converts function_execute timeout from seconds to milliseconds for copilot calls', async () => {
isKnownTool.mockReturnValue(false)
isSimExecuted.mockReturnValue(false)
executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
await executeTool(
'function_execute',
{ code: 'return 1', timeout: 7 },
{
userId: 'user-1',
workflowId: 'workflow-1',
workspaceId: 'ws-1',
copilotToolExecution: true,
}
)
expect(executeAppTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
timeout: 7000,
_context: expect.objectContaining({
copilotToolExecution: true,
}),
})
)
})
it('defaults copilot function_execute timeout to 10 seconds when omitted', async () => {
isKnownTool.mockReturnValue(false)
isSimExecuted.mockReturnValue(false)
executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
await executeTool(
'function_execute',
{ code: 'return 1' },
{
userId: 'user-1',
workflowId: 'workflow-1',
workspaceId: 'ws-1',
copilotToolExecution: true,
}
)
expect(executeAppTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
timeout: 10_000,
})
)
})
it('defaults copilot function_execute timeout to 10 seconds when invalid', async () => {
isKnownTool.mockReturnValue(false)
isSimExecuted.mockReturnValue(false)
executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
await executeTool(
'function_execute',
{ code: 'return 1', timeout: 0 },
{
userId: 'user-1',
workflowId: 'workflow-1',
workspaceId: 'ws-1',
copilotToolExecution: true,
}
)
expect(executeAppTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
timeout: 10_000,
})
)
})
it('does not let copilot function_execute timeout exceed the default execution limit', async () => {
isKnownTool.mockReturnValue(false)
isSimExecuted.mockReturnValue(false)
executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
await executeTool(
'function_execute',
{ code: 'return 1', timeout: 10_000 },
{
userId: 'user-1',
workflowId: 'workflow-1',
workspaceId: 'ws-1',
copilotToolExecution: true,
}
)
expect(executeAppTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
timeout: DEFAULT_EXECUTION_TIMEOUT_MS,
})
)
})
})
@@ -0,0 +1,154 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
import { executeTool as executeAppTool } from '@/tools'
import { isClientExecuted, isKnownTool, isSimExecuted } from './router'
import type {
ToolCallDescriptor,
ToolExecutionContext,
ToolExecutionResult,
ToolHandler,
} from './types'
const logger = createLogger('ToolExecutor')
const FUNCTION_EXECUTE_TOOL_ID = 'function_execute'
const DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS = 10
const MILLISECONDS_PER_SECOND = 1000
const handlerRegistry = new Map<string, ToolHandler>()
export function registerHandler(toolId: string, handler: ToolHandler): void {
handlerRegistry.set(toolId, handler)
}
export function registerHandlers(entries: Record<string, ToolHandler>): void {
for (const [toolId, handler] of Object.entries(entries)) {
handlerRegistry.set(toolId, handler)
}
}
export function getRegisteredToolIds(): string[] {
return Array.from(handlerRegistry.keys())
}
export function hasHandler(toolId: string): boolean {
return handlerRegistry.has(toolId)
}
export function clearHandlers(): void {
handlerRegistry.clear()
}
export async function executeTool(
toolId: string,
params: Record<string, unknown>,
context: ToolExecutionContext
): Promise<ToolExecutionResult> {
// Client-routed tools (e.g. run_workflow) are normally executed in the browser and never
// reach this point in interactive mode. In headless mode (Mothership block, no browser) there
// is no client to delegate to, so fall back to the registered server-side handler when one
// exists — otherwise the call would route to executeAppTool and throw "Tool not found".
const canUseRegisteredHandler =
isKnownTool(toolId) &&
(isSimExecuted(toolId) || (isClientExecuted(toolId) && hasHandler(toolId)))
if (!canUseRegisteredHandler) {
const appParams = buildAppToolParams(toolId, params, context)
return executeAppTool(toolId, appParams)
}
if (context.abortSignal?.aborted) {
logger.warn('Tool execution skipped: abort signal already set', {
toolId,
abortReason: context.abortSignal.reason ?? 'unknown',
})
return { success: false, error: 'Execution aborted: abort signal was set before tool started' }
}
const handler = handlerRegistry.get(toolId)
if (!handler) {
logger.warn('No handler registered for tool', { toolId })
return { success: false, error: `No handler for tool: ${toolId}` }
}
try {
return await handler(params, context)
} catch (error) {
const message = toError(error).message
logger.error('Tool execution failed', {
toolId,
error: message,
abortSignalAborted: context.abortSignal?.aborted ?? false,
})
return { success: false, error: message }
}
}
async function executeToolBatch(
toolCalls: ToolCallDescriptor[],
context: ToolExecutionContext
): Promise<Map<string, ToolExecutionResult>> {
const results = new Map<string, ToolExecutionResult>()
const executions = toolCalls.map(async ({ toolCallId, toolId, params }) => {
const result = await executeTool(toolId, params, context)
results.set(toolCallId, result)
})
await Promise.allSettled(executions)
for (const { toolCallId } of toolCalls) {
if (!results.has(toolCallId)) {
results.set(toolCallId, {
success: false,
error: 'Tool execution did not produce a result',
})
}
}
return results
}
function buildAppToolParams(
toolId: string,
params: Record<string, unknown>,
context: ToolExecutionContext
): Record<string, unknown> {
const result = { ...params }
if (toolId === FUNCTION_EXECUTE_TOOL_ID && context.copilotToolExecution) {
const rawTimeoutSeconds =
result.timeout === undefined || result.timeout === null
? DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS
: Number(result.timeout)
const timeoutSeconds =
Number.isFinite(rawTimeoutSeconds) && rawTimeoutSeconds > 0
? rawTimeoutSeconds
: DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS
result.timeout = Math.min(
Math.ceil(timeoutSeconds * MILLISECONDS_PER_SECOND),
DEFAULT_EXECUTION_TIMEOUT_MS
)
}
if (result.credentialId && !result.credential && !result.oauthCredential) {
result.credential = result.credentialId
}
result._context = {
...(typeof result._context === 'object' && result._context !== null
? (result._context as object)
: {}),
userId: context.userId,
workflowId: context.workflowId,
workspaceId: context.workspaceId,
chatId: context.chatId,
executionId: context.executionId,
runId: context.runId,
copilotToolExecution: context.copilotToolExecution,
requestMode: context.requestMode,
currentAgentId: context.currentAgentId,
enforceCredentialAccess: true,
}
return result
}
@@ -0,0 +1,3 @@
export { executeTool } from './executor'
export { ensureHandlersRegistered } from './register-handlers'
export { getToolEntry, isSimExecuted } from './router'
@@ -0,0 +1,206 @@
import { createLogger } from '@sim/logger'
import {
CheckDeploymentStatus,
CompleteScheduledTask,
CreateWorkflow,
CreateWorkspaceMcpServer,
DeleteWorkflow,
DeleteWorkspaceMcpServer,
DeployApi,
DeployChat,
DeployCustomBlock,
DeployMcp,
DiffWorkflows,
FunctionExecute,
GenerateApiKey,
GetBlockOutputs,
GetBlockUpstreamReferences,
GetDeployedWorkflowState,
GetDeploymentLog,
GetPlatformActions,
GetWorkflowData,
GetWorkflowRunOptions,
Glob as GlobTool,
Grep as GrepTool,
ListIntegrationTools,
ListUserWorkspaces,
ListWorkspaceMcpServers,
LoadDeployment,
ManageCredential,
ManageCustomTool,
ManageFolder,
ManageMcpTool,
ManageScheduledTask,
ManageSkill,
MaterializeFile,
MoveWorkflow,
OauthGetAuthLink,
OauthRequestAccess,
OpenResource,
PromoteToLive,
Read as ReadTool,
Redeploy,
RenameWorkflow,
RestoreResource,
RunBlock,
RunFromBlock,
RunWorkflow,
RunWorkflowUntilBlock,
SetBlockEnabled,
SetGlobalWorkflowVariables,
UpdateDeploymentVersion,
UpdateScheduledTaskHistory,
UpdateWorkspaceMcpServer,
} from '@/lib/copilot/generated/tool-catalog-v1'
import { createServerToolHandler } from '@/lib/copilot/tools/registry/server-tool-adapter'
import { getRegisteredServerToolNames } from '@/lib/copilot/tools/server/router'
import { executeDeployCustomBlock } from '../tools/handlers/deployment/custom-block'
import {
executeDeployApi,
executeDeployChat,
executeDeployMcp,
executeRedeploy,
} from '../tools/handlers/deployment/deploy'
import {
executeCheckDeploymentStatus,
executeCreateWorkspaceMcpServer,
executeDeleteWorkspaceMcpServer,
executeDiffWorkflows,
executeGetDeploymentLog,
executeListWorkspaceMcpServers,
executeLoadDeployment,
executePromoteToLive,
executeUpdateDeploymentVersion,
executeUpdateWorkspaceMcpServer,
} from '../tools/handlers/deployment/manage'
import { executeFunctionExecute } from '../tools/handlers/function-execute'
import { executeListIntegrationTools } from '../tools/handlers/integration-tools'
import {
executeCompleteJob,
executeManageJob,
executeUpdateJobHistory,
} from '../tools/handlers/jobs'
import { executeManageCredential } from '../tools/handlers/management/manage-credential'
import { executeManageCustomTool } from '../tools/handlers/management/manage-custom-tool'
import { executeManageMcpTool } from '../tools/handlers/management/manage-mcp-tool'
import { executeManageSkill } from '../tools/handlers/management/manage-skill'
import { executeMaterializeFile } from '../tools/handlers/materialize-file'
import { executeOAuthGetAuthLink, executeOAuthRequestAccess } from '../tools/handlers/oauth'
import { executeGetPlatformActions } from '../tools/handlers/platform'
import { executeOpenResource } from '../tools/handlers/resources'
import { executeRestoreResource } from '../tools/handlers/restore-resource'
import { executeVfsGlob, executeVfsGrep, executeVfsRead } from '../tools/handlers/vfs'
import {
executeCreateWorkflow,
executeDeleteWorkflow,
executeGenerateApiKey,
executeManageFolder,
executeMoveWorkflow,
executeRenameWorkflow,
executeRunBlock,
executeRunFromBlock,
executeRunWorkflow,
executeRunWorkflowUntilBlock,
executeSetBlockEnabled,
executeSetGlobalWorkflowVariables,
} from '../tools/handlers/workflow/mutations'
import {
executeGetBlockOutputs,
executeGetBlockUpstreamReferences,
executeGetDeployedWorkflowState,
executeGetWorkflowData,
executeGetWorkflowRunOptions,
executeListUserWorkspaces,
} from '../tools/handlers/workflow/queries'
import { registerHandlers } from './executor'
import type { ToolHandler } from './types'
const logger = createLogger('ToolHandlerRegistration')
let registered = false
export function ensureHandlersRegistered(): void {
if (registered) return
registered = true
registerHandlers(buildHandlerMap())
logger.info('Tool handlers registered')
}
// Bridge: handler implementations accept specific param types (e.g. CreateWorkflowParams)
// while ToolHandler accepts Record<string, unknown>. The params are cast internally by
// each implementation. ExecutionContext extends ToolExecutionContext so context is compatible.
function h(fn: (params: any, context: any) => Promise<any>): ToolHandler {
return fn as ToolHandler
}
function buildHandlerMap(): Record<string, ToolHandler> {
return {
[ListUserWorkspaces.id]: h((_p, c) => executeListUserWorkspaces(c)),
[GetWorkflowData.id]: h(executeGetWorkflowData),
[GetWorkflowRunOptions.id]: h(executeGetWorkflowRunOptions),
[GetBlockOutputs.id]: h(executeGetBlockOutputs),
[GetBlockUpstreamReferences.id]: h(executeGetBlockUpstreamReferences),
[GetDeployedWorkflowState.id]: h(executeGetDeployedWorkflowState),
[CreateWorkflow.id]: h(executeCreateWorkflow),
[DeleteWorkflow.id]: h(executeDeleteWorkflow),
[RenameWorkflow.id]: h(executeRenameWorkflow),
[MoveWorkflow.id]: h(executeMoveWorkflow),
[ManageFolder.id]: h(executeManageFolder),
[RunWorkflow.id]: h(executeRunWorkflow),
[RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock),
[RunFromBlock.id]: h(executeRunFromBlock),
[RunBlock.id]: h(executeRunBlock),
[SetBlockEnabled.id]: h(executeSetBlockEnabled),
[GenerateApiKey.id]: h(executeGenerateApiKey),
[SetGlobalWorkflowVariables.id]: h(executeSetGlobalWorkflowVariables),
[DeployApi.id]: h(executeDeployApi),
[DeployChat.id]: h(executeDeployChat),
[DeployMcp.id]: h(executeDeployMcp),
[DeployCustomBlock.id]: h(executeDeployCustomBlock),
[Redeploy.id]: h(executeRedeploy),
[CheckDeploymentStatus.id]: h(executeCheckDeploymentStatus),
[ListWorkspaceMcpServers.id]: h(executeListWorkspaceMcpServers),
[CreateWorkspaceMcpServer.id]: h(executeCreateWorkspaceMcpServer),
[UpdateWorkspaceMcpServer.id]: h(executeUpdateWorkspaceMcpServer),
[DeleteWorkspaceMcpServer.id]: h(executeDeleteWorkspaceMcpServer),
[GetDeploymentLog.id]: h(executeGetDeploymentLog),
[DiffWorkflows.id]: h(executeDiffWorkflows),
[LoadDeployment.id]: h(executeLoadDeployment),
[PromoteToLive.id]: h(executePromoteToLive),
[UpdateDeploymentVersion.id]: h(executeUpdateDeploymentVersion),
[ManageScheduledTask.id]: h(executeManageJob),
[CompleteScheduledTask.id]: h(executeCompleteJob),
[UpdateScheduledTaskHistory.id]: h(executeUpdateJobHistory),
[GrepTool.id]: h(executeVfsGrep),
[GlobTool.id]: h(executeVfsGlob),
[ReadTool.id]: h(executeVfsRead),
[ManageCustomTool.id]: h(executeManageCustomTool),
[ManageMcpTool.id]: h(executeManageMcpTool),
[ManageSkill.id]: h(executeManageSkill),
[ManageCredential.id]: h(executeManageCredential),
[OauthGetAuthLink.id]: h(executeOAuthGetAuthLink),
[OauthRequestAccess.id]: h(executeOAuthRequestAccess),
[OpenResource.id]: h(executeOpenResource),
[RestoreResource.id]: h(executeRestoreResource),
[GetPlatformActions.id]: h(executeGetPlatformActions),
[ListIntegrationTools.id]: h(executeListIntegrationTools),
[MaterializeFile.id]: h(executeMaterializeFile),
[FunctionExecute.id]: h(executeFunctionExecute),
...buildServerToolHandlers(),
}
}
function buildServerToolHandlers(): Record<string, ToolHandler> {
const toolNames = getRegisteredServerToolNames()
const handlers: Record<string, ToolHandler> = {}
for (const toolId of toolNames) {
handlers[toolId] = createServerToolHandler(toolId)
}
return handlers
}
@@ -0,0 +1,63 @@
import { TOOL_CATALOG, type ToolCatalogEntry } from '@/lib/copilot/generated/tool-catalog-v1'
import type { ToolCallDescriptor } from './types'
export type ToolRouteTarget = ToolCatalogEntry['route']
export function isToolInCatalog(toolId: string): boolean {
return toolId in TOOL_CATALOG
}
export function getToolEntry(toolId: string): ToolCatalogEntry | undefined {
return TOOL_CATALOG[toolId]
}
export type ToolRoute = {
route: ToolRouteTarget
mode: ToolCatalogEntry['mode']
subagentId?: string
}
export function routeToolCall(toolId: string): ToolRoute | null {
const entry = getToolEntry(toolId)
if (!entry) return null
return { route: entry.route, mode: entry.mode, subagentId: entry.subagentId }
}
export function isSimExecuted(toolId: string): boolean {
return getToolEntry(toolId)?.route === 'sim'
}
export function isGoExecuted(toolId: string): boolean {
return getToolEntry(toolId)?.route === 'go'
}
export function isClientExecuted(toolId: string): boolean {
return getToolEntry(toolId)?.route === 'client'
}
export function isKnownTool(toolId: string): boolean {
return isToolInCatalog(toolId)
}
interface PartitionedBatch {
sim: ToolCallDescriptor[]
go: ToolCallDescriptor[]
subagent: ToolCallDescriptor[]
client: ToolCallDescriptor[]
unknown: ToolCallDescriptor[]
}
export function partitionToolBatch(toolCalls: ToolCallDescriptor[]): PartitionedBatch {
const result: PartitionedBatch = { sim: [], go: [], subagent: [], client: [], unknown: [] }
for (const tc of toolCalls) {
const route = routeToolCall(tc.toolId)
if (!route) {
result.unknown.push(tc)
continue
}
result[route.route].push(tc)
}
return result
}
@@ -0,0 +1,43 @@
import type { MothershipResource } from '@/lib/copilot/resources/types'
export interface ToolExecutionContext {
userId: string
workflowId: string
workspaceId?: string
chatId?: string
messageId?: string
executionId?: string
runId?: string
copilotToolExecution?: boolean
requestMode?: string
currentAgentId?: string
/**
* The invoking subagent's channel id (its outer tool_use id), threaded per
* tool call so server tools can scope state to one subagent invocation. Two
* concurrent file subagents share currentAgentId ("file") but have distinct
* parentToolCallIds, so this — not currentAgentId — disambiguates them.
*/
parentToolCallId?: string
abortSignal?: AbortSignal
userTimezone?: string
userPermission?: string
decryptedEnvVars?: Record<string, string>
}
export interface ToolExecutionResult {
success: boolean
output?: unknown
error?: string
resources?: MothershipResource[]
}
export type ToolHandler = (
params: Record<string, unknown>,
context: ToolExecutionContext
) => Promise<ToolExecutionResult>
export interface ToolCallDescriptor {
toolCallId: string
toolId: string
params: Record<string, unknown>
}