Skip to content

Commit 2dc92a3

Browse files
committed
fix(copilot): surface actionable server tool errors
1 parent c490747 commit 2dc92a3

10 files changed

Lines changed: 67 additions & 23 deletions

File tree

apps/sim/lib/copilot/tools/server/files/doc-extract.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OrchestrationError } from '@/lib/core/orchestration/types'
12
import { CodeLanguage } from '@/lib/execution/languages'
23
import { executeInSandbox } from '@/lib/execution/remote-sandbox'
34

@@ -28,7 +29,10 @@ export interface DocExtract {
2829
export async function extractDocText(args: { binary: Buffer; ext: string }): Promise<DocExtract> {
2930
const ext = args.ext.toLowerCase()
3031
if (!isExtractableDocExt(ext)) {
31-
throw new Error(`Cannot extract text from .${ext} (supported: pdf, pptx, docx, xlsx)`)
32+
throw new OrchestrationError(
33+
'validation',
34+
`Cannot extract text from .${ext} (supported: pdf, pptx, docx, xlsx)`
35+
)
3236
}
3337

3438
const script = `
@@ -102,7 +106,7 @@ print("__SIM_RESULT__=" + json.dumps({"text": text}))
102106
})
103107

104108
if (result.error) {
105-
throw new Error(`Document extraction failed: ${result.error}`)
109+
throw new OrchestrationError('validation', `Document extraction failed: ${result.error}`)
106110
}
107111
const payload = result.result as { text?: string } | null
108112
const full = payload?.text ?? ''

apps/sim/lib/copilot/tools/server/files/doc-render.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OrchestrationError } from '@/lib/core/orchestration/types'
12
import { CodeLanguage } from '@/lib/execution/languages'
23
import { executeInSandbox } from '@/lib/execution/remote-sandbox'
34

@@ -37,7 +38,10 @@ export async function renderDocToGrid(args: {
3738
}): Promise<DocRender> {
3839
const ext = args.ext.toLowerCase()
3940
if (!isRenderableDocExt(ext)) {
40-
throw new Error(`Cannot render .${ext} to images (supported: pptx, docx, pdf)`)
41+
throw new OrchestrationError(
42+
'validation',
43+
`Cannot render .${ext} to images (supported: pptx, docx, pdf)`
44+
)
4145
}
4246

4347
const script = `
@@ -99,7 +103,7 @@ else:
99103
})
100104

101105
if (result.error) {
102-
throw new Error(`Document render failed: ${result.error}`)
106+
throw new OrchestrationError('validation', `Document render failed: ${result.error}`)
103107
}
104108
const payload = result.result as { grid?: string | null; pageCount?: number } | null
105109
if (!payload?.grid) {

apps/sim/lib/copilot/tools/server/files/file-folder-application.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { executeCopilotFileUseCase } from '@/lib/copilot/application/execute-file-use-case'
22
import type { CopilotFileDelegationContext } from '@/lib/copilot/auth/file-delegation'
3+
import { OrchestrationError } from '@/lib/core/orchestration/types'
34
import { findWorkspaceFileFolderIdByPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
45
import { createWorkspaceFileFolderOperation } from '@/lib/workspace-files/application/workspace-file-folders'
56

@@ -13,7 +14,10 @@ export function requireCopilotWorkspace(
1314
): string {
1415
if (!context.workspaceId) throw new Error('Copilot execution workspace is required')
1516
if (assertedWorkspaceId && assertedWorkspaceId !== context.workspaceId) {
16-
throw new Error('Workspace ID does not match the Copilot execution workspace')
17+
throw new OrchestrationError(
18+
'validation',
19+
'Workspace ID does not match the Copilot execution workspace'
20+
)
1721
}
1822
return context.workspaceId
1923
}

apps/sim/lib/copilot/tools/server/generated-schema.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'
22
import { TOOL_RUNTIME_SCHEMAS } from '@/lib/copilot/generated/tool-schemas-v1'
3+
import { OrchestrationError } from '@/lib/core/orchestration/types'
34

45
const ajv = new Ajv({
56
allErrors: true,
@@ -42,7 +43,13 @@ export function validateGeneratedToolPayload<T>(
4243

4344
if (!validator(payload)) {
4445
const label = schemaKind === 'parameters' ? 'input' : 'output'
45-
throw new Error(`${toolName} ${label} validation failed: ${formatErrors(validator.errors)}`)
46+
const message = `${toolName} ${label} validation failed: ${formatErrors(validator.errors)}`
47+
// Input validation is the CALLER's mistake — classified, so the copilot
48+
// error projection surfaces it verbatim and the model can fix its
49+
// arguments instead of blind-retrying a masked "system error". Output
50+
// validation failing is the tool's own bug and stays internal.
51+
if (schemaKind === 'parameters') throw new OrchestrationError('validation', message)
52+
throw new Error(message)
4653
}
4754

4855
return payload

apps/sim/lib/copilot/tools/server/other/search-online.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { WebSearch } from '@/lib/copilot/generated/tool-catalog-v1'
44
import { projectToolErrorMessageForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result'
55
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
66
import { env } from '@/lib/core/config/env'
7+
import { OrchestrationError } from '@/lib/core/orchestration/types'
78
import { executeTool } from '@/tools'
89

910
interface OnlineSearchParams {
@@ -35,7 +36,8 @@ export const searchOnlineServerTool: BaseServerTool<OnlineSearchParams, SearchRe
3536
async execute(params: OnlineSearchParams, context?: ServerToolContext): Promise<SearchResponse> {
3637
const logger = createLogger('SearchOnlineServerTool')
3738
const { query, num = 10, type = 'search', gl, hl } = params
38-
if (!query || typeof query !== 'string') throw new Error('query is required')
39+
if (!query || typeof query !== 'string')
40+
throw new OrchestrationError('validation', 'query is required')
3941

4042
const hasExaApiKey = Boolean(env.EXA_API_KEY && String(env.EXA_API_KEY).length > 0)
4143
const hasSerperApiKey = Boolean(env.SERPER_API_KEY && String(env.SERPER_API_KEY).length > 0)
@@ -104,7 +106,8 @@ export const searchOnlineServerTool: BaseServerTool<OnlineSearchParams, SearchRe
104106
}
105107

106108
if (!hasSerperApiKey) {
107-
throw new Error(
109+
throw new OrchestrationError(
110+
'forbidden',
108111
'Web search is not configured on this Sim deployment and cannot be enabled from a tool. Answer from the workspace instead (grep/glob/read, search_sim_docs) or tell the user web search is unavailable.'
109112
)
110113
}
@@ -126,7 +129,9 @@ export const searchOnlineServerTool: BaseServerTool<OnlineSearchParams, SearchRe
126129

127130
if (!result.success) {
128131
const errorMsg = (result as { error?: string }).error ?? 'Search failed'
129-
throw new Error(errorMsg)
132+
// Classified so the provider's actual failure (rate limit, bad query)
133+
// reaches the model instead of the generic system-error mask.
134+
throw new OrchestrationError('conflict', errorMsg)
130135
}
131136

132137
return {

apps/sim/lib/copilot/tools/server/router.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-cr
6161
import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables'
6262
import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit-workflow'
6363
import { queryLogsServerTool } from '@/lib/copilot/tools/server/workflow/query-logs'
64+
import { OrchestrationError } from '@/lib/core/orchestration/types'
6465
import { listCustomBlocksWithInputsForWorkspace } from '@/lib/workflows/custom-blocks/operations'
6566
import { withCustomBlockOverlay } from '@/blocks/custom/server-overlay'
6667
import { withBlockVisibility } from '@/blocks/visibility/server-context'
@@ -219,7 +220,7 @@ export async function routeExecution(
219220
): Promise<unknown> {
220221
const tool = getServerToolRegistry()[toolName]
221222
if (!tool) {
222-
throw new Error(`Unknown server tool: ${toolName}`)
223+
throw new OrchestrationError('validation', `Unknown server tool: ${toolName}`)
223224
}
224225

225226
logger.debug(
@@ -233,7 +234,10 @@ export async function routeExecution(
233234
const action = (p?.operation ?? p?.action) as string | undefined
234235
if (isWriteAction(toolName, action) && !copilotToolCanWrite(context?.userPermission)) {
235236
const actionLabel = action ? `'${action}' on ` : ''
236-
throw new Error(
237+
// Classified so the projection surfaces it: a permission denial is
238+
// caller-actionable (stop retrying, tell the user), not a system error.
239+
throw new OrchestrationError(
240+
'forbidden',
237241
`Permission denied: ${actionLabel}${toolName} requires write access. You have '${context?.userPermission ?? 'none'}' permission.`
238242
)
239243
}

apps/sim/lib/copilot/tools/server/user/get-credentials.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { decodeJwt } from 'jose'
77
import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions'
88
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
99
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags'
10+
import { OrchestrationError } from '@/lib/core/orchestration/types'
1011
import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
1112
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
1213
import { createIntegrationCredentialVisibility } from '@/lib/integrations/credential-visibility.server'
@@ -50,7 +51,7 @@ export const getCredentialsServerTool: BaseServerTool<GetCredentialsParams, any>
5051
workflowId: params.workflowId,
5152
authenticatedUserId,
5253
})
53-
throw new Error(errorMessage)
54+
throw new OrchestrationError('forbidden', errorMessage)
5455
}
5556

5657
workspaceId = wId

apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getDefaultWorkspaceId,
88
} from '@/lib/copilot/tools/handlers/access'
99
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
10+
import { OrchestrationError } from '@/lib/core/orchestration/types'
1011
import { performUpdateCredential } from '@/lib/credentials/orchestration'
1112
import { listVisibleWorkspaceCredentials } from '@/lib/credentials/queries'
1213
import { upsertPersonalEnvVars, upsertWorkspaceEnvVars } from '@/lib/environment/utils'
@@ -67,7 +68,8 @@ function normalizeDescriptions(
6768
if (!item || typeof item.name !== 'string' || item.description === undefined) continue
6869
const description = item.description?.trim() ?? ''
6970
if (description.length > DESCRIPTION_MAX_LENGTH) {
70-
throw new Error(
71+
throw new OrchestrationError(
72+
'validation',
7173
`description for ${item.name} must be at most ${DESCRIPTION_MAX_LENGTH} characters`
7274
)
7375
}
@@ -159,7 +161,10 @@ async function resolveWorkspaceId(
159161
if (params.workflowId) {
160162
const { workflow } = await ensureWorkflowAccess(params.workflowId, userId, 'write')
161163
if (!workflow.workspaceId) {
162-
throw new Error(`Workflow ${params.workflowId} is not associated with a workspace`)
164+
throw new OrchestrationError(
165+
'validation',
166+
`Workflow ${params.workflowId} is not associated with a workspace`
167+
)
163168
}
164169
return workflow.workspaceId
165170
}
@@ -202,7 +207,10 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<
202207
// per-workspace mirrors, so there is no single row to hold its description —
203208
// one written here would exist in this workspace alone.
204209
if (scope === 'personal' && Object.keys(descriptions).length > 0) {
205-
throw new Error('description is only supported for a workspace secret')
210+
throw new OrchestrationError(
211+
'validation',
212+
'description is only supported for a workspace secret'
213+
)
206214
}
207215
const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized })
208216
const variableNames = Object.keys(validatedVariables)
@@ -249,7 +257,10 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<
249257
// A failed description never fails a stored value — but a describe-only call
250258
// has nothing else to report, so its failure is the result.
251259
if (descriptionFailures.length > 0 && workspaceUpdated.length === 0) {
252-
throw new Error(`Could not describe: ${descriptionFailures.join('; ')}`)
260+
throw new OrchestrationError(
261+
'conflict',
262+
`Could not describe: ${descriptionFailures.join('; ')}`
263+
)
253264
}
254265

255266
const parts: string[] = []

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
101101
const logger = createLogger('EditWorkflowServerTool')
102102
const { operations, workflowId, currentUserWorkflow } = params
103103
if (!Array.isArray(operations) || operations.length === 0) {
104-
throw new Error('operations are required and must be an array')
104+
throw new OrchestrationError('validation', 'operations are required and must be an array')
105105
}
106-
if (!workflowId) throw new Error('workflowId is required')
106+
if (!workflowId) throw new OrchestrationError('validation', 'workflowId is required')
107107
if (!context?.userId) {
108108
throw new Error('Unauthorized workflow access')
109109
}
@@ -135,7 +135,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
135135
operationsReferenceSimSandbox(operations) &&
136136
(!workspaceId || !(await hasWorkspaceSandboxAccess(workspaceId)))
137137
) {
138-
throw new Error(MAX_PLAN_REQUIRED)
138+
throw new OrchestrationError('forbidden', MAX_PLAN_REQUIRED)
139139
}
140140

141141
logger.info('Executing edit_workflow', {
@@ -153,7 +153,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
153153
workflowState = JSON.parse(currentUserWorkflow)
154154
} catch (error) {
155155
logger.error('Failed to parse currentUserWorkflow', error)
156-
throw new Error('Invalid currentUserWorkflow format')
156+
throw new OrchestrationError('validation', 'Invalid currentUserWorkflow format')
157157
}
158158
} else {
159159
const fromDb = await getCurrentWorkflowStateFromDb(workflowId)
@@ -251,7 +251,10 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
251251
errors: validation.errors,
252252
warnings: validation.warnings,
253253
})
254-
throw new Error(`Invalid edited workflow: ${validation.errors.join('; ')}`)
254+
throw new OrchestrationError(
255+
'validation',
256+
`Invalid edited workflow: ${validation.errors.join('; ')}`
257+
)
255258
}
256259

257260
if (validation.warnings.length > 0) {
@@ -387,7 +390,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
387390
workflowId,
388391
error: saveResult.error,
389392
})
390-
throw new Error(`Failed to save workflow: ${saveResult.error}`)
393+
throw new OrchestrationError('conflict', `Failed to save workflow: ${saveResult.error}`)
391394
}
392395

393396
// Update workflow's lastSynced timestamp

apps/sim/lib/copilot/tools/server/workflow/query-logs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { z } from 'zod'
33
import { QueryLogs } from '@/lib/copilot/generated/tool-catalog-v1'
44
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
5+
import { OrchestrationError } from '@/lib/core/orchestration/types'
56
import {
67
collectLargeValueExecutionIds,
78
collectLargeValueKeys,
@@ -120,7 +121,7 @@ type QueryLogsArgs = z.infer<typeof queryLogsArgsSchema>
120121
function resolveWorkspaceId(args: QueryLogsArgs, context?: ServerToolContext): string {
121122
const workspaceId = args.workspaceId ?? context?.workspaceId
122123
if (!workspaceId) {
123-
throw new Error('workspaceId is required')
124+
throw new OrchestrationError('validation', 'workspaceId is required')
124125
}
125126
return workspaceId
126127
}

0 commit comments

Comments
 (0)