diff --git a/services/platform/app/features/custom-agents/components/tool-selector.tsx b/services/platform/app/features/custom-agents/components/tool-selector.tsx index 3050eb641f..82195da94e 100644 --- a/services/platform/app/features/custom-agents/components/tool-selector.tsx +++ b/services/platform/app/features/custom-agents/components/tool-selector.tsx @@ -50,7 +50,7 @@ const TOOL_CATEGORIES: Record = { ], Integrations: ['integration', 'integration_batch', 'integration_introspect'], Data: ['database_schema'], - Other: ['verify_approval', 'request_human_input'], + Other: ['request_human_input'], }; function categorizeTools(toolNames: string[]) { diff --git a/services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx b/services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx index 49a72e066a..4caf49b51f 100644 --- a/services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx +++ b/services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx @@ -22,7 +22,6 @@ import { SUPPORTED_TEMPLATE_VARIABLES } from '@/convex/lib/agent_response/resolv import { STRUCTURED_RESPONSE_INSTRUCTIONS } from '@/convex/lib/agent_response/structured_response_instructions'; import { toId } from '@/convex/lib/type_cast_helpers'; import { useT } from '@/lib/i18n/client'; -import { FILE_PREPROCESSING_INSTRUCTIONS } from '@/lib/shared/constants/custom-agents'; import { seo } from '@/lib/utils/seo'; export const Route = createFileRoute( @@ -42,7 +41,6 @@ export const Route = createFileRoute( interface InstructionsFormData { systemInstructions: string; modelId: string; - filePreprocessingEnabled: boolean; structuredResponsesEnabled: boolean; } @@ -98,7 +96,6 @@ function InstructionsTab() { ? { systemInstructions: agent.systemInstructions, modelId: initialModelId, - filePreprocessingEnabled: agent.filePreprocessingEnabled ?? false, structuredResponsesEnabled: agent.structuredResponsesEnabled ?? true, } : undefined, @@ -117,7 +114,6 @@ function InstructionsTab() { systemInstructions: data.systemInstructions, modelPreset, modelId: data.modelId, - filePreprocessingEnabled: data.filePreprocessingEnabled, structuredResponsesEnabled: data.structuredResponsesEnabled, }); }, @@ -196,34 +192,6 @@ function InstructionsTab() { /> - - { - form.setValue('filePreprocessingEnabled', checked); - void save({ - ...form.getValues(), - filePreprocessingEnabled: checked, - }); - }} - label={t('customAgents.form.filePreprocessingEnabled')} - description={t('customAgents.form.filePreprocessingEnabledHelp')} - disabled={isReadOnly} - /> - {formValues.filePreprocessingEnabled && ( - - {FILE_PREPROCESSING_INSTRUCTIONS} - - )} - - { const hidden = new Set(); hidden.add('rag_search'); - if (webSearchMode !== 'off') { - hidden.add('web'); - } + hidden.add('web'); return hidden; - }, [webSearchMode]); + }, []); // Auto-save web search mode const webModeData = useMemo(() => ({ webSearchMode }), [webSearchMode]); diff --git a/services/platform/convex/_generated/api.d.ts b/services/platform/convex/_generated/api.d.ts index 06e63916f2..d5f4d9dbf6 100644 --- a/services/platform/convex/_generated/api.d.ts +++ b/services/platform/convex/_generated/api.d.ts @@ -60,7 +60,6 @@ import type * as agent_tools_integrations_integration_tool from "../agent_tools/ import type * as agent_tools_integrations_internal_actions from "../agent_tools/integrations/internal_actions.js"; import type * as agent_tools_integrations_internal_mutations from "../agent_tools/integrations/internal_mutations.js"; import type * as agent_tools_integrations_types from "../agent_tools/integrations/types.js"; -import type * as agent_tools_integrations_verify_approval_tool from "../agent_tools/integrations/verify_approval_tool.js"; import type * as agent_tools_load_convex_tools_as_object from "../agent_tools/load_convex_tools_as_object.js"; import type * as agent_tools_location_internal_mutations from "../agent_tools/location/internal_mutations.js"; import type * as agent_tools_location_mutations from "../agent_tools/location/mutations.js"; @@ -928,7 +927,6 @@ declare const fullApi: ApiFromModules<{ "agent_tools/integrations/internal_actions": typeof agent_tools_integrations_internal_actions; "agent_tools/integrations/internal_mutations": typeof agent_tools_integrations_internal_mutations; "agent_tools/integrations/types": typeof agent_tools_integrations_types; - "agent_tools/integrations/verify_approval_tool": typeof agent_tools_integrations_verify_approval_tool; "agent_tools/load_convex_tools_as_object": typeof agent_tools_load_convex_tools_as_object; "agent_tools/location/internal_mutations": typeof agent_tools_location_internal_mutations; "agent_tools/location/mutations": typeof agent_tools_location_mutations; diff --git a/services/platform/convex/agent_tools/integrations/verify_approval_tool.ts b/services/platform/convex/agent_tools/integrations/verify_approval_tool.ts deleted file mode 100644 index a32e7a01a4..0000000000 --- a/services/platform/convex/agent_tools/integrations/verify_approval_tool.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Convex Tool: Verify Approval - * - * Allows the AI to verify that an approval record actually exists in the database - * before claiming it was created. This prevents hallucination of approval IDs. - */ - -import type { ToolCtx } from '@convex-dev/agent'; - -import { createTool } from '@convex-dev/agent'; -import { z } from 'zod/v4'; - -import type { ToolDefinition } from '../types'; - -import { internal } from '../../_generated/api'; -import { toId } from '../../lib/type_cast_helpers'; - -const verifyApprovalArgs = z.object({ - approvalId: z.string().describe('Approval ID returned from write operation'), -}); - -export interface VerifyApprovalResult { - exists: boolean; - approvalId: string; - status?: 'pending' | 'executing' | 'completed' | 'rejected'; - operationName?: string; - operationTitle?: string; - integrationName?: string; - error?: string; -} - -export const verifyApprovalTool: ToolDefinition = { - name: 'verify_approval', - tool: createTool({ - description: `Verify an approval record exists. -Call after write operations to confirm approval was created.`, - - args: verifyApprovalArgs, - - handler: async (ctx: ToolCtx, args): Promise => { - const { approvalId } = args; - - // Validate the approval ID format (Convex IDs start with specific prefixes) - if (!approvalId || typeof approvalId !== 'string') { - return { - exists: false, - approvalId: approvalId || '', - error: 'Invalid approval ID: must be a non-empty string', - }; - } - - try { - // Query the approval from the database - // The ID comes from LLM output, so we cast it to the expected type - // The query will throw if the ID format is invalid - const approval = await ctx.runQuery( - internal.approvals.internal_queries.getApprovalById, - { - approvalId: toId<'approvals'>(approvalId), - }, - ); - - if (!approval) { - console.warn( - '[verify_approval] Approval not found - possible hallucination:', - { - approvalId, - }, - ); - return { - exists: false, - approvalId, - error: `Approval with ID "${approvalId}" does not exist in the database. The approval may not have been created successfully.`, - }; - } - - // Extract metadata for integration operations - const operationName = - typeof approval.metadata?.operationName === 'string' - ? approval.metadata.operationName - : undefined; - const operationTitle = - typeof approval.metadata?.operationTitle === 'string' - ? approval.metadata.operationTitle - : undefined; - const integrationName = - typeof approval.metadata?.integrationName === 'string' - ? approval.metadata.integrationName - : undefined; - - console.log('[verify_approval] Approval verified successfully:', { - approvalId, - status: approval.status, - operationName, - integrationName, - }); - - return { - exists: true, - approvalId, - status: approval.status, - operationName, - operationTitle, - integrationName, - }; - } catch (error) { - // Handle invalid ID format errors from Convex - const errorMessage = - error instanceof Error ? error.message : String(error); - - console.error('[verify_approval] Verification failed:', { - approvalId, - error: errorMessage, - }); - - return { - exists: false, - approvalId, - error: `Failed to verify approval: ${errorMessage}`, - }; - } - }, - }), -} as const; diff --git a/services/platform/convex/agent_tools/tool_names.ts b/services/platform/convex/agent_tools/tool_names.ts index a3c769505b..9ff0e655c1 100644 --- a/services/platform/convex/agent_tools/tool_names.ts +++ b/services/platform/convex/agent_tools/tool_names.ts @@ -29,7 +29,6 @@ export const TOOL_NAMES = [ 'integration', 'integration_batch', 'integration_introspect', - 'verify_approval', 'database_schema', 'request_human_input', 'document_find', diff --git a/services/platform/convex/agent_tools/tool_registry.ts b/services/platform/convex/agent_tools/tool_registry.ts index 500f724d01..d305c55e56 100644 --- a/services/platform/convex/agent_tools/tool_registry.ts +++ b/services/platform/convex/agent_tools/tool_registry.ts @@ -23,7 +23,6 @@ import { requestHumanInputTool } from './human_input/request_human_input_tool'; import { integrationBatchTool } from './integrations/integration_batch_tool'; import { integrationIntrospectTool } from './integrations/integration_introspect_tool'; import { integrationTool } from './integrations/integration_tool'; -import { verifyApprovalTool } from './integrations/verify_approval_tool'; import { requestUserLocationTool } from './location/request_user_location_tool'; import { productReadTool } from './products/product_read_tool'; import { ragSearchTool } from './rag/rag_search_tool'; @@ -61,7 +60,6 @@ export const TOOL_REGISTRY = [ integrationTool, integrationBatchTool, integrationIntrospectTool, - verifyApprovalTool, databaseSchemaTool, requestHumanInputTool, documentFindTool, diff --git a/services/platform/convex/agent_tools/workflows/internal_mutations.ts b/services/platform/convex/agent_tools/workflows/internal_mutations.ts index b04be2c749..ea2640f8dc 100644 --- a/services/platform/convex/agent_tools/workflows/internal_mutations.ts +++ b/services/platform/convex/agent_tools/workflows/internal_mutations.ts @@ -12,10 +12,7 @@ import { jsonRecordValidator } from '../../../lib/shared/schemas/utils/json-valu import { components, internal } from '../../_generated/api'; import { internalMutation } from '../../_generated/server'; import { createApproval } from '../../approvals/helpers'; -import { - createCustomAgentHookHandles, - toSerializableConfig, -} from '../../custom_agents/config'; +import { toSerializableConfig } from '../../custom_agents/config'; import { getDefaultAgentRuntimeConfig } from '../../lib/agent_runtime_config'; import { checkOrganizationRateLimit } from '../../lib/rate_limiter/helpers'; import { persistentStreaming } from '../../streaming/helpers'; @@ -141,11 +138,6 @@ export const triggerWorkflowCompletionResponse = internalMutation({ }); } - const hooks = await createCustomAgentHookHandles( - ctx, - chatAgent.filePreprocessingEnabled, - ); - await ctx.scheduler.runAfter( 0, internal.lib.agent_chat.internal_actions.runAgentGeneration, @@ -156,7 +148,6 @@ export const triggerWorkflowCompletionResponse = internalMutation({ provider, debugTag: `[Agent:${chatAgent.name}:WorkflowComplete]`, enableStreaming: true, - hooks, threadId, organizationId, promptMessage: messageContent, diff --git a/services/platform/convex/agents/integration/agent.ts b/services/platform/convex/agents/integration/agent.ts index 66bd8ff9f9..8804da6981 100644 --- a/services/platform/convex/agents/integration/agent.ts +++ b/services/platform/convex/agents/integration/agent.ts @@ -29,7 +29,6 @@ You do not access the internal customer/product database — for that, the user - integration: Execute a single operation on an integration - integration_batch: Execute multiple parallel read operations - integration_introspect: Discover available integrations and their operations -- verify_approval: Verify approval card was created **INTEGRATION NAMES** Only use integrations listed in "## Available Integrations". Never guess names. @@ -109,7 +108,6 @@ Your workflow for write operations: Understanding the response: - \`requiresApproval: true\` + \`approvalId\` = SUCCESS! Approval card was created - \`approvalCreated: true\` = Confirmation that card exists -- You can optionally call \`verify_approval(approvalId)\` to double-check it exists CRITICAL RULES: - NEVER call write operations without ALL required parameter values @@ -133,7 +131,6 @@ export function createIntegrationAgent(options?: { 'integration', 'integration_batch', 'integration_introspect', - 'verify_approval', ]; debugLog('createIntegrationAgent', { diff --git a/services/platform/convex/custom_agents/config.ts b/services/platform/convex/custom_agents/config.ts index ddfe542d23..42179a05a4 100644 --- a/services/platform/convex/custom_agents/config.ts +++ b/services/platform/convex/custom_agents/config.ts @@ -10,27 +10,16 @@ * - 'advanced' → OPENAI_CODING_MODEL */ -import { createFunctionHandle, makeFunctionReference } from 'convex/server'; - import type { Doc } from '../_generated/dataModel'; -import type { MutationCtx } from '../_generated/server'; import type { ToolName } from '../agent_tools/tool_names'; -import type { - AgentHooksConfig, - SerializableAgentConfig, -} from '../lib/agent_chat/types'; +import type { SerializableAgentConfig } from '../lib/agent_chat/types'; -import { FILE_PREPROCESSING_INSTRUCTIONS } from '../../lib/shared/constants/custom-agents'; import { getCodingModelOrThrow, getDefaultModel, getFastModel, } from '../lib/agent_runtime_config'; -const beforeGenerateHookRef = makeFunctionReference<'action'>( - 'lib/agent_chat/internal_actions:beforeGenerateHook', -); - function resolveModel(agent: Doc<'customAgents'>): string { if (agent.modelId) return agent.modelId; @@ -47,20 +36,14 @@ function resolveModel(agent: Doc<'customAgents'>): string { export function toSerializableConfig( agent: Doc<'customAgents'>, ): SerializableAgentConfig { - const instructions = agent.filePreprocessingEnabled - ? [agent.systemInstructions, FILE_PREPROCESSING_INSTRUCTIONS] - .filter(Boolean) - .join('\n\n') - : agent.systemInstructions; - const knowledgeMode = agent.knowledgeMode ?? (agent.knowledgeEnabled ? 'tool' : 'off'); const webSearchMode = agent.webSearchMode ?? (agent.toolNames.includes('web') ? 'tool' : 'off'); return { - name: agent.isSystemDefault ? agent.name : `custom:${agent.name}`, - instructions, + name: `${agent.name}:v${agent.versionNumber}`, + instructions: agent.systemInstructions, // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- toolNames are filtered via filterValidToolNames() on insert; always valid ToolName values convexToolNames: agent.toolNames as ToolName[], integrationBindings: agent.integrationBindings, @@ -84,20 +67,3 @@ export function toSerializableConfig( outputReserve: agent.outputReserve, }; } - -/** - * Create FunctionHandles for custom agent file preprocessing hooks. - * Returns undefined when file preprocessing is disabled. - */ -export async function createCustomAgentHookHandles( - _ctx: MutationCtx, - filePreprocessingEnabled: boolean | undefined, -): Promise { - if (!filePreprocessingEnabled) { - return undefined; - } - - const beforeGenerate = await createFunctionHandle(beforeGenerateHookRef); - - return { beforeGenerate }; -} diff --git a/services/platform/convex/custom_agents/mutations.ts b/services/platform/convex/custom_agents/mutations.ts index c3e8d4c066..22bb9f9503 100644 --- a/services/platform/convex/custom_agents/mutations.ts +++ b/services/platform/convex/custom_agents/mutations.ts @@ -50,7 +50,6 @@ const agentFieldsValidator = { knowledgeEnabled: v.optional(v.boolean()), includeOrgKnowledge: v.optional(v.boolean()), knowledgeTopK: v.optional(v.number()), - filePreprocessingEnabled: v.optional(v.boolean()), structuredResponsesEnabled: v.optional(v.boolean()), teamId: v.optional(v.string()), delegateAgentIds: v.optional(v.array(v.id('customAgents'))), @@ -343,7 +342,6 @@ export const updateCustomAgent = mutation({ includeOrgKnowledge: v.optional(v.boolean()), includeTeamKnowledge: v.optional(v.boolean()), knowledgeTopK: v.optional(v.number()), - filePreprocessingEnabled: v.optional(v.boolean()), structuredResponsesEnabled: v.optional(v.boolean()), teamId: v.optional(v.string()), conversationStarters: v.optional(v.array(v.string())), diff --git a/services/platform/convex/custom_agents/schema.ts b/services/platform/convex/custom_agents/schema.ts index a38a71090c..6510f5f26b 100644 --- a/services/platform/convex/custom_agents/schema.ts +++ b/services/platform/convex/custom_agents/schema.ts @@ -68,6 +68,7 @@ export const customAgentsTable = defineTable({ includeTeamKnowledge: v.optional(v.boolean()), knowledgeFiles: v.optional(v.array(knowledgeFileValidator)), knowledgeTopK: v.optional(v.number()), + // @deprecated — kept for existing documents; no longer written or read filePreprocessingEnabled: v.optional(v.boolean()), structuredResponsesEnabled: v.optional(v.boolean()), diff --git a/services/platform/convex/custom_agents/seed_system_defaults.ts b/services/platform/convex/custom_agents/seed_system_defaults.ts index 279bfa00c5..d9cabd8d2c 100644 --- a/services/platform/convex/custom_agents/seed_system_defaults.ts +++ b/services/platform/convex/custom_agents/seed_system_defaults.ts @@ -54,7 +54,6 @@ async function insertSystemAgent( roleRestriction: template.roleRestriction, knowledgeEnabled: template.knowledgeEnabled, includeOrgKnowledge: template.includeOrgKnowledge, - filePreprocessingEnabled: template.filePreprocessingEnabled, conversationStarters: template.conversationStarters, visibleInChat: template.visibleInChat, isSystemDefault: true, diff --git a/services/platform/convex/custom_agents/system_defaults.ts b/services/platform/convex/custom_agents/system_defaults.ts index bca665e063..99fd9cb933 100644 --- a/services/platform/convex/custom_agents/system_defaults.ts +++ b/services/platform/convex/custom_agents/system_defaults.ts @@ -32,7 +32,6 @@ export interface SystemDefaultAgentTemplate { roleRestriction?: 'admin_developer'; knowledgeEnabled?: boolean; includeOrgKnowledge?: boolean; - filePreprocessingEnabled?: boolean; visibleInChat?: boolean; publishOnSeed?: boolean; } @@ -54,7 +53,6 @@ export const SYSTEM_DEFAULT_AGENT_TEMPLATES: SystemDefaultAgentTemplate[] = [ ], knowledgeEnabled: true, includeOrgKnowledge: true, - filePreprocessingEnabled: true, maxSteps: 20, timeoutMs: 1_200_000, outputReserve: 4096, @@ -118,12 +116,7 @@ export const SYSTEM_DEFAULT_AGENT_TEMPLATES: SystemDefaultAgentTemplate[] = [ displayName: 'Integration Assistant', description: 'Connects and operates with external systems', systemInstructions: INTEGRATION_AGENT_INSTRUCTIONS, - toolNames: [ - 'integration', - 'integration_batch', - 'integration_introspect', - 'verify_approval', - ], + toolNames: ['integration', 'integration_batch', 'integration_introspect'], delegateSlugs: [], maxSteps: 20, timeoutMs: 180_000, @@ -153,7 +146,6 @@ export const SYSTEM_DEFAULT_AGENT_TEMPLATES: SystemDefaultAgentTemplate[] = [ outputReserve: 2048, modelPreset: 'advanced', roleRestriction: 'admin_developer', - filePreprocessingEnabled: true, publishOnSeed: true, }, ]; diff --git a/services/platform/convex/custom_agents/test_chat.test.ts b/services/platform/convex/custom_agents/test_chat.test.ts index 38b47f06cf..6a1b9c7950 100644 --- a/services/platform/convex/custom_agents/test_chat.test.ts +++ b/services/platform/convex/custom_agents/test_chat.test.ts @@ -41,7 +41,7 @@ describe('testCustomAgent', () => { const config = toSerializableConfig(draft); expect(config).toEqual({ - name: 'custom:test-agent', + name: 'test-agent:v1', instructions: 'You are a helpful test agent.', convexToolNames: ['web_search', 'document_search'], integrationBindings: ['integration_1'], @@ -103,35 +103,6 @@ describe('testCustomAgent', () => { }); }); - describe('file preprocessing instructions', () => { - it('should not append preprocessing instructions when disabled', () => { - const draft = createMockDraftAgent({ filePreprocessingEnabled: false }); - const config = toSerializableConfig(draft); - - expect(config.instructions).toBe('You are a helpful test agent.'); - expect(config.instructions).not.toContain('FILE ATTACHMENTS'); - }); - - it('should not append preprocessing instructions when undefined', () => { - const draft = createMockDraftAgent({ - filePreprocessingEnabled: undefined, - }); - const config = toSerializableConfig(draft); - - expect(config.instructions).toBe('You are a helpful test agent.'); - expect(config.instructions).not.toContain('FILE ATTACHMENTS'); - }); - - it('should append preprocessing instructions when enabled', () => { - const draft = createMockDraftAgent({ filePreprocessingEnabled: true }); - const config = toSerializableConfig(draft); - - expect(config.instructions).toContain('You are a helpful test agent.'); - expect(config.instructions).toContain('**FILE ATTACHMENTS**'); - expect(config.instructions).toContain('PRE-ANALYZED CONTENT'); - }); - }); - describe('toSerializableConfig retrieval modes', () => { it('should default knowledgeMode to off when no legacy fields', () => { const draft = createMockDraftAgent({ diff --git a/services/platform/convex/custom_agents/test_chat.ts b/services/platform/convex/custom_agents/test_chat.ts index a24bcf1e47..dddd22b654 100644 --- a/services/platform/convex/custom_agents/test_chat.ts +++ b/services/platform/convex/custom_agents/test_chat.ts @@ -14,7 +14,7 @@ import { startAgentChat } from '../lib/agent_chat'; import { getDefaultAgentRuntimeConfig } from '../lib/agent_runtime_config'; import { getUserTeamIds } from '../lib/get_user_teams'; import { hasTeamAccess } from '../lib/team_access'; -import { createCustomAgentHookHandles, toSerializableConfig } from './config'; +import { toSerializableConfig } from './config'; export const testCustomAgent = mutation({ args: { @@ -58,11 +58,6 @@ export const testCustomAgent = mutation({ const agentConfig = toSerializableConfig(agent); const { model, provider } = getDefaultAgentRuntimeConfig(); - const hooks = await createCustomAgentHookHandles( - ctx, - agent.filePreprocessingEnabled, - ); - return startAgentChat({ ctx, agentType: 'custom', @@ -74,9 +69,8 @@ export const testCustomAgent = mutation({ agentConfig, model: agentConfig.model ?? model, provider, - debugTag: `[CustomAgent:${agent.name}:test]`, + debugTag: `[${agent.name}:v${agent.versionNumber}:test]`, enableStreaming: true, - hooks, customAgentId: args.customAgentId, }); }, diff --git a/services/platform/convex/custom_agents/unified_chat.ts b/services/platform/convex/custom_agents/unified_chat.ts index 9f46f485d8..abc0b9bcb5 100644 --- a/services/platform/convex/custom_agents/unified_chat.ts +++ b/services/platform/convex/custom_agents/unified_chat.ts @@ -16,7 +16,7 @@ import { getDefaultAgentRuntimeConfig } from '../lib/agent_runtime_config'; import { getUserTeamIds } from '../lib/get_user_teams'; import { getOrganizationMember } from '../lib/rls'; import { hasTeamAccess } from '../lib/team_access'; -import { createCustomAgentHookHandles, toSerializableConfig } from './config'; +import { toSerializableConfig } from './config'; export const chatWithAgent = mutation({ args: { @@ -101,11 +101,6 @@ export const chatWithAgent = mutation({ const agentConfig = toSerializableConfig(activeVersion); const { model, provider } = getDefaultAgentRuntimeConfig(); - const hooks = await createCustomAgentHookHandles( - ctx, - activeVersion.filePreprocessingEnabled, - ); - return startAgentChat({ ctx, agentType: 'custom', @@ -119,9 +114,8 @@ export const chatWithAgent = mutation({ agentConfig, model: agentConfig.model ?? model, provider, - debugTag: `[Agent:${activeVersion.name}]`, + debugTag: `[${activeVersion.name}:v${activeVersion.versionNumber}]`, enableStreaming: true, - hooks, customAgentId: rootVersionId, }); }, diff --git a/services/platform/convex/custom_agents/webhooks/internal_mutations.ts b/services/platform/convex/custom_agents/webhooks/internal_mutations.ts index b9df49e300..4d3c5f4be4 100644 --- a/services/platform/convex/custom_agents/webhooks/internal_mutations.ts +++ b/services/platform/convex/custom_agents/webhooks/internal_mutations.ts @@ -4,7 +4,7 @@ import { internalMutation } from '../../_generated/server'; import { startAgentChat } from '../../lib/agent_chat'; import { getDefaultAgentRuntimeConfig } from '../../lib/agent_runtime_config'; import { createChatThread } from '../../threads/create_chat_thread'; -import { createCustomAgentHookHandles, toSerializableConfig } from '../config'; +import { toSerializableConfig } from '../config'; export const updateWebhookLastTriggered = internalMutation({ args: { @@ -69,11 +69,6 @@ export const chatViaWebhook = internalMutation({ const agentConfig = toSerializableConfig(activeVersion); const { model, provider } = getDefaultAgentRuntimeConfig(); - const hooks = await createCustomAgentHookHandles( - ctx, - activeVersion.filePreprocessingEnabled, - ); - const result = await startAgentChat({ ctx, agentType: 'custom', @@ -84,9 +79,8 @@ export const chatViaWebhook = internalMutation({ agentConfig, model: agentConfig.model ?? model, provider, - debugTag: `[CustomAgent:webhook:${activeVersion.name}]`, + debugTag: `[${activeVersion.name}:v${activeVersion.versionNumber}:webhook]`, enableStreaming: args.enableStreaming ?? true, - hooks, customAgentId: args.customAgentId, }); diff --git a/services/platform/convex/lib/agent_completion/on_agent_complete.ts b/services/platform/convex/lib/agent_completion/on_agent_complete.ts index eed67435d7..ad43d31847 100644 --- a/services/platform/convex/lib/agent_completion/on_agent_complete.ts +++ b/services/platform/convex/lib/agent_completion/on_agent_complete.ts @@ -52,6 +52,7 @@ export interface AgentResponseResult { hasRag: boolean; hasWebContext: boolean; }; + error?: string; } export interface OnAgentCompleteArgs { @@ -100,6 +101,7 @@ export async function onAgentComplete( toolsUsage: result.toolsUsage, contextWindow: result.contextWindow, contextStats: result.contextStats, + error: result.error, }, ); diff --git a/services/platform/convex/lib/agent_response/generate_response.ts b/services/platform/convex/lib/agent_response/generate_response.ts index 79defd3a00..c9bbd7af47 100644 --- a/services/platform/convex/lib/agent_response/generate_response.ts +++ b/services/platform/convex/lib/agent_response/generate_response.ts @@ -227,10 +227,35 @@ export async function generateAgentResponse( const startTime = Date.now(); const abortController = new AbortController(); - // Declared outside try so the catch block can access them for cleanup + // Declared outside try so the catch block can access them for cleanup/metadata let abortWatcher: AbortWatcher | undefined; let baselineAbortedIds = new Set(); + // Hoisted so partial data is available in the catch block for error metadata + let structuredThreadContext: + | { + threadContext: string; + stats: { + totalTokens: number; + messageCount: number; + approvalCount: number; + hasRag: boolean; + hasWebContext: boolean; + }; + } + | undefined; + let agentInstructions: string | undefined; + let retrySystemMessageId: string | undefined; + let firstTokenTime: number | null = null; + let savedMessageId: string | undefined; + let result: { + text?: string; + steps?: unknown[]; + usage?: GenerateResponseResult['usage']; + finishReason?: string; + response?: { modelId?: string }; + } = {}; + try { debugLog(`generate${capitalize(agentType)}Response called`, { threadId, @@ -409,7 +434,7 @@ export async function generateAgentResponse( // Build structured context (history, RAG, web) // Note: promptMessage is NOT included - it's passed via `prompt` parameter const agentConfig = AGENT_CONTEXT_CONFIGS[agentType]; - const structuredThreadContext = await buildStructuredContext({ + structuredThreadContext = await buildStructuredContext({ ctx, threadId, additionalContext, @@ -476,24 +501,9 @@ export async function generateAgentResponse( knowledgeFileIds, }; - // Track time to first token for streaming - let firstTokenTime: number | null = null; - - // The first saved message ID for this generation, used for metadata and approval linking. - // Captured from the agent SDK's savedMessages before any retry logic can overwrite `result`. - let savedMessageId: string | undefined; let didRetry = false; let retryInProgress = false; - // Generate response - streaming or non-streaming - let result: { - text?: string; - steps?: unknown[]; - usage?: GenerateResponseResult['usage']; - finishReason?: string; - response?: { modelId?: string }; - }; - const promptToSend = hookPromptContent ?? promptMessage; // Resolve template variables (e.g. {{organization.name}}, {{current_time}}) @@ -513,7 +523,7 @@ export async function generateAgentResponse( // and the structured thread context (history, RAG, web search). // For streaming agents, append structured response instructions so the LLM // can optionally emit section markers (parsed by the frontend). - const agentInstructions = + agentInstructions = enableStreaming && resolvedInstructions && structuredResponsesEnabled !== false @@ -779,13 +789,14 @@ export async function generateAgentResponse( } // Save system message to record the continuation in thread history - await saveMessage(ctx, components.agent, { + const retryMsg = await saveMessage(ctx, components.agent, { threadId, message: { role: 'system', content: '[RESPONSE_INTERRUPTED] Retrying…', }, }); + retrySystemMessageId = retryMsg.messageId; // Prevent zombie detection during the gap before continuation saves its own message const originalSavedMessageId = savedMessageId; @@ -834,6 +845,27 @@ export async function generateAgentResponse( response: result.response, }; + // Update the "Retrying…" system message now that retry succeeded + if (retrySystemMessageId) { + try { + await ctx.runMutation(components.agent.messages.updateMessage, { + messageId: retrySystemMessageId, + patch: { + message: { + role: 'system', + content: '[RESPONSE_INTERRUPTED] Retry succeeded', + }, + }, + }); + } catch (updateError) { + console.error( + '[generateAgentResponse] Failed to update retry system message on success:', + updateError, + ); + } + retrySystemMessageId = undefined; + } + debugLog('Continue completed', { reason: continueCheck.reason, textLength: result.text?.length ?? 0, @@ -1331,7 +1363,28 @@ export async function generateAgentResponse( } } + // Update "Retrying…" system message to indicate retry failed + if (retrySystemMessageId) { + try { + await ctx.runMutation(components.agent.messages.updateMessage, { + messageId: retrySystemMessageId, + patch: { + message: { + role: 'system', + content: '[RESPONSE_INTERRUPTED] Retry failed', + }, + }, + }); + } catch (retryMsgError) { + console.error( + '[generateAgentResponse] Failed to update retry system message:', + retryMsgError, + ); + } + } + // Save failed message — skip if cancelGeneration already created one + let failedMessageId: string | undefined; try { const msgs = await listMessages(ctx, components.agent, { threadId, @@ -1343,17 +1396,25 @@ export async function generateAgentResponse( ); const hasFailedAssistant = newestAssistant?.status === 'failed'; if (!hasFailedAssistant) { - await saveMessage(ctx, components.agent, { - threadId, - message: { - role: 'assistant', - content: 'I was unable to complete your request. Please try again.', - }, - metadata: { - status: 'failed', - error: errorMessage || 'Unknown error', + const { messageId: failedMsgId } = await saveMessage( + ctx, + components.agent, + { + threadId, + message: { + role: 'assistant', + content: + 'I was unable to complete your request. Please try again.', + }, + metadata: { + status: 'failed', + error: errorMessage || 'Unknown error', + }, }, - }); + ); + failedMessageId = failedMsgId; + } else { + failedMessageId = newestAssistant._id; } } catch (saveError) { console.error( @@ -1362,6 +1423,59 @@ export async function generateAgentResponse( ); } + // Record partial metadata for debugging even on failure + const metadataMessageId = savedMessageId ?? failedMessageId; + if (metadataMessageId) { + try { + const durationMs = Date.now() - startTime; + const { toolCalls, toolsUsage } = extractToolCallsFromSteps( + result.steps ?? [], + ); + const contextWindowParts: string[] = []; + if (agentInstructions) { + contextWindowParts.push( + wrapInDetails('📋 System Prompt', agentInstructions), + ); + } + if (toolsSummary) { + contextWindowParts.push(wrapInDetails('🔧 Tools', toolsSummary)); + } + if (structuredThreadContext) { + contextWindowParts.push(structuredThreadContext.threadContext); + } + + await onAgentComplete(ctx, { + threadId, + agentType, + result: { + threadId, + messageId: metadataMessageId, + text: '', + model: result.response?.modelId ?? model, + provider, + usage: result.usage, + durationMs, + timeToFirstTokenMs: firstTokenTime + ? firstTokenTime - startTime + : undefined, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + toolsUsage: toolsUsage.length > 0 ? toolsUsage : undefined, + contextWindow: + contextWindowParts.length > 0 + ? contextWindowParts.join('\n\n') + : undefined, + contextStats: structuredThreadContext?.stats, + error: errorMessage || 'Unknown error', + }, + }); + } catch (metadataError) { + console.error( + '[generateAgentResponse] Failed to save error metadata:', + metadataError, + ); + } + } + throw error; } } diff --git a/services/platform/convex/message_metadata/internal_mutations.ts b/services/platform/convex/message_metadata/internal_mutations.ts index 7cbea94048..51f724a359 100644 --- a/services/platform/convex/message_metadata/internal_mutations.ts +++ b/services/platform/convex/message_metadata/internal_mutations.ts @@ -27,6 +27,7 @@ export const saveMessageMetadata = internalMutation({ toolsUsage: v.optional(v.array(toolUsageItemValidator)), contextWindow: v.optional(v.string()), contextStats: v.optional(contextStatsValidator), + error: v.optional(v.string()), }, returns: v.id('messageMetadata'), handler: async (ctx, args) => { @@ -61,6 +62,7 @@ export const saveMessageMetadata = internalMutation({ toolsUsage: args.toolsUsage ?? existing.toolsUsage, contextWindow: contextWindow ?? existing.contextWindow, contextStats: args.contextStats ?? existing.contextStats, + error: args.error ?? existing.error, }); return existing._id; } @@ -82,6 +84,7 @@ export const saveMessageMetadata = internalMutation({ toolsUsage: args.toolsUsage, contextWindow, contextStats: args.contextStats, + error: args.error, }); }, }); diff --git a/services/platform/convex/streaming/schema.ts b/services/platform/convex/streaming/schema.ts index d383a2e951..4b6688fc9b 100644 --- a/services/platform/convex/streaming/schema.ts +++ b/services/platform/convex/streaming/schema.ts @@ -60,6 +60,7 @@ export const messageMetadataTable = defineTable({ hasIntegrations: v.optional(v.boolean()), }), ), + error: v.optional(v.string()), }) .index('by_messageId', ['messageId']) .index('by_threadId', ['threadId']); diff --git a/services/platform/convex/streaming/validators.ts b/services/platform/convex/streaming/validators.ts index 0d9c46dd71..e0bfe3bd7d 100644 --- a/services/platform/convex/streaming/validators.ts +++ b/services/platform/convex/streaming/validators.ts @@ -47,4 +47,5 @@ export const messageMetadataValidator = v.object({ toolsUsage: v.optional(v.array(toolUsageItemValidator)), contextWindow: v.optional(v.string()), contextStats: v.optional(contextStatsValidator), + error: v.optional(v.string()), }); diff --git a/services/platform/convex/workflow_engine/helpers/recovery/recover_stuck_executions.ts b/services/platform/convex/workflow_engine/helpers/recovery/recover_stuck_executions.ts index a73bd337ea..34d56e7304 100644 --- a/services/platform/convex/workflow_engine/helpers/recovery/recover_stuck_executions.ts +++ b/services/platform/convex/workflow_engine/helpers/recovery/recover_stuck_executions.ts @@ -73,7 +73,14 @@ async function cancelComponentWorkflow( // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- componentWorkflowId is stored as string but the component API requires WorkflowId branded type const workflowId = execution.componentWorkflowId as unknown as WorkflowId; - await manager.cancel(ctx, workflowId); + try { + await manager.cancel(ctx, workflowId); + } catch (error) { + console.warn( + `[StuckRecovery] Failed to cancel workflow ${execution.componentWorkflowId}, it may have already been cleaned up:`, + error, + ); + } await ctx.scheduler.runAfter( CLEANUP_DELAY_MS, internal.workflow_engine.internal_mutations.cleanupComponentWorkflow, diff --git a/services/platform/lib/shared/constants/custom-agents.ts b/services/platform/lib/shared/constants/custom-agents.ts index 3bf4315d77..7f466b06f9 100644 --- a/services/platform/lib/shared/constants/custom-agents.ts +++ b/services/platform/lib/shared/constants/custom-agents.ts @@ -1,18 +1,2 @@ export const MAX_CONVERSATION_STARTERS = 4; export const MAX_CONVERSATION_STARTER_LENGTH = 200; - -/** - * Prompt suffix injected into custom agent system instructions when - * file preprocessing is enabled. Shared between backend (config.ts) - * and frontend (instructions page preview). - */ -export const FILE_PREPROCESSING_INSTRUCTIONS = `**FILE ATTACHMENTS** -If the user's CURRENT message contains "[PRE-ANALYZED CONTENT" or sections like: -• "**Document: filename.pdf**" followed by content -• "**Image: filename.jpg**" followed by description -• "**Text File: filename.txt**" followed by analysis - -These are pre-analyzed attachments from the CURRENT message. -Answer the user's question directly from this content. -Do NOT use file processing tools for content that is already provided. -For files marked as "[ATTACHED FILES]" without pre-analysis, use your tools to process them.`; diff --git a/services/platform/lib/shared/schemas/custom_agents.ts b/services/platform/lib/shared/schemas/custom_agents.ts index 10f94d2fc0..13c85fd316 100644 --- a/services/platform/lib/shared/schemas/custom_agents.ts +++ b/services/platform/lib/shared/schemas/custom_agents.ts @@ -23,7 +23,6 @@ const customAgentSchema = z.object({ knowledgeEnabled: z.boolean().optional(), includeOrgKnowledge: z.boolean().optional(), knowledgeTopK: z.number().optional(), - filePreprocessingEnabled: z.boolean().optional(), structuredResponsesEnabled: z.boolean().optional(), conversationStarters: z.array(z.string()).optional(), teamId: z.string().optional(), @@ -57,7 +56,6 @@ const createCustomAgentSchema = z.object({ knowledgeEnabled: z.boolean().optional(), includeOrgKnowledge: z.boolean().optional(), knowledgeTopK: z.number().int().min(1).max(50).optional(), - filePreprocessingEnabled: z.boolean().optional(), structuredResponsesEnabled: z.boolean().optional(), conversationStarters: z.array(z.string().max(200)).max(4).optional(), teamId: z.string().optional(), diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json index 434c60aa5a..f663c037a8 100644 --- a/services/platform/messages/en.json +++ b/services/platform/messages/en.json @@ -789,11 +789,6 @@ "teamNone": "Organization-wide", "sharedWithTeams": "Share with additional teams", "sharedWithTeamsHelp": "Select additional teams that can access this agent", - "sectionFilePreprocessing": "File preprocessing", - "sectionFilePreprocessingDescription": "Configure how file attachments are processed", - "filePreprocessingEnabled": "Pre-analyze file attachments", - "filePreprocessingEnabledHelp": "When enabled, uploaded files (documents, images, text) are automatically analyzed before the AI responds. This provides faster and more accurate responses. When disabled, the AI uses tools to read files on demand.", - "filePreprocessingInjectedPrompt": "The following instructions will be automatically appended to the system prompt:", "copyPrompt": "Copy prompt", "sectionStructuredResponses": "Structured responses", "sectionStructuredResponsesDescription": "Configure whether the agent can use structured response markers (conclusion, key points, details, etc.) to format substantial answers",