diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts index b34ee88977..9c6dcf0844 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts @@ -8,6 +8,8 @@ import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service' import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service'; +import { AGENT_RUN_BASE_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const'; +import { STRUCTURED_OUTPUT_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/structured-output-system-prompt.const'; import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { NATIVE_WEB_SEARCH_COST_PER_CALL_DOLLARS } from 'src/engine/metadata-modules/ai/ai-billing/constants/native-web-search-cost-per-call-dollars'; import { AiBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service'; @@ -64,6 +66,18 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti modelConfiguration: {}, }) as AgentEntity; + const emptyUsage = { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + inputTokenDetails: { + noCacheTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + }, + outputTokenDetails: { textTokens: 0, reasoningTokens: 0 }, + }; + beforeEach(async () => { toolRegistry = { getToolsByCategories: jest.fn().mockResolvedValue({}) }; roleTargetRepository = { findOne: jest.fn() }; @@ -138,6 +152,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti await service.executeAgent({ agent: buildAgent(), userPrompt: 'test', + baseSystemPrompt: 'base system prompt', workspaceId, }); @@ -158,12 +173,65 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti await service.executeAgent({ agent: buildAgent(), userPrompt: 'test', + baseSystemPrompt: 'base system prompt', workspaceId, }); expect(toolRegistry.getToolsByCategories).not.toHaveBeenCalled(); }); + it('prefixes the system prompt with the caller-supplied base prompt', async () => { + roleTargetRepository.findOne.mockResolvedValueOnce(null); + + await service.executeAgent({ + agent: buildAgent(), + userPrompt: 'test', + baseSystemPrompt: 'caller base prompt', + workspaceId, + }); + + expect(generateTextMock).toHaveBeenCalledWith( + expect.objectContaining({ + system: 'caller base prompt\n\ntest prompt', + }), + ); + }); + + it('uses the context-neutral structured output prompt regardless of the caller base prompt', async () => { + roleTargetRepository.findOne.mockResolvedValueOnce(null); + generateTextMock + .mockResolvedValueOnce({ + text: 'execution result', + steps: [], + usage: emptyUsage, + } as unknown as Awaited>) + .mockResolvedValueOnce({ + text: '', + steps: [], + usage: emptyUsage, + output: { summary: 'done' }, + } as unknown as Awaited>); + + await service.executeAgent({ + agent: { + ...buildAgent(), + responseFormat: { + type: 'json', + schema: { type: 'object', properties: {} }, + }, + } as AgentEntity, + userPrompt: 'test', + baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT, + workspaceId, + }); + + expect(generateTextMock).toHaveBeenCalledTimes(2); + expect(generateTextMock.mock.calls[1][0].system).toBe( + STRUCTURED_OUTPUT_SYSTEM_PROMPT, + ); + expect(generateTextMock.mock.calls[1][0].system).not.toMatch(/workflow/i); + }); + describe('cost folding', () => { const baseUsage = { inputTokens: 100, @@ -191,6 +259,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti const result = await service.executeAgent({ agent: buildAgent(), userPrompt: 'test', + baseSystemPrompt: 'base system prompt', workspaceId, }); @@ -264,6 +333,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti const result = await service.executeAgent({ agent: buildAgent(), userPrompt: 'test', + baseSystemPrompt: 'base system prompt', workspaceId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-run.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-run.service.spec.ts index cfc2fe0bb5..10a9feda97 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-run.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-run.service.spec.ts @@ -4,6 +4,7 @@ import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service'; import { AgentRunService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service'; +import { AGENT_RUN_BASE_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { getWorkspaceScopedRepositoryToken } from 'src/engine/twenty-orm/workspace-scoped-repository/get-workspace-scoped-repository-token.util'; @@ -95,6 +96,20 @@ describe('AgentRunService', () => { ); }); + it('runs the agent with the programmatic base system prompt', async () => { + await service.run({ + workspace, + requestUserWorkspaceId: null, + input, + }); + + expect(agentAsyncExecutorService.executeAgent).toHaveBeenCalledWith( + expect.objectContaining({ + baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT, + }), + ); + }); + it('returns an error result when the workspace ran out of credits', async () => { agentAsyncExecutorService.executeAgent.mockResolvedValue({ result: { response: 'partial' }, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts index 3efaf642c4..951b0b1a37 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts @@ -33,7 +33,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { WORKFLOW_AGENT_REGISTRY_TOOL_CATEGORIES } from 'src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-registry-tool-categories.const'; import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type'; import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const'; -import { WORKFLOW_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const'; +import { STRUCTURED_OUTPUT_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/structured-output-system-prompt.const'; import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util'; import { NATIVE_WEB_SEARCH_COST_PER_CALL_DOLLARS } from 'src/engine/metadata-modules/ai/ai-billing/constants/native-web-search-cost-per-call-dollars'; @@ -75,9 +75,10 @@ const EMPTY_USAGE: LanguageModelUsage = { }, }; -// Agent execution within workflows uses registry tools plus native model tools. -// Workflow registry tools are intentionally excluded to avoid circular -// dependencies and recursive workflow execution. +// Agent execution uses registry tools plus native model tools. The caller +// supplies the base system prompt describing its execution context (workflow +// step, programmatic run). Workflow registry tools are intentionally excluded +// to avoid circular dependencies and recursive workflow execution. @Injectable() export class AgentAsyncExecutorService { private readonly logger = new Logger(AgentAsyncExecutorService.name); @@ -113,6 +114,7 @@ export class AgentAsyncExecutorService { async executeAgent({ agent, userPrompt, + baseSystemPrompt, actorContext, authContext, workspaceId, @@ -121,6 +123,7 @@ export class AgentAsyncExecutorService { }: { agent: AgentEntity | null; userPrompt: string; + baseSystemPrompt: string; actorContext?: ActorMetadata; authContext?: WorkspaceAuthContext; workspaceId: string; @@ -230,7 +233,7 @@ export class AgentAsyncExecutorService { let hasNoMoreAvailableCredits = false; const textResponse = await generateText({ - system: `${WORKFLOW_SYSTEM_PROMPTS.BASE}\n\n${agent ? agent.prompt : ''}`, + system: `${baseSystemPrompt}\n\n${agent ? agent.prompt : ''}`, tools, model: registeredModel.model, prompt: userPrompt, @@ -335,7 +338,7 @@ export class AgentAsyncExecutorService { if (agentSchema) { const structuredResult = await generateText({ - system: WORKFLOW_SYSTEM_PROMPTS.OUTPUT_GENERATOR, + system: STRUCTURED_OUTPUT_SYSTEM_PROMPT, model: registeredModel.model, prompt: `Based on the following execution results, generate the structured output according to the schema: diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service.ts index 88943427f9..ca02ffd82b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service.ts @@ -10,6 +10,7 @@ import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/wo import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum'; import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type'; import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service'; +import { AGENT_RUN_BASE_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; @@ -64,6 +65,7 @@ export class AgentRunService { await this.agentAsyncExecutorService.executeAgent({ agent, userPrompt: input.prompt, + baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT, authContext, workspaceId: workspace.id, userWorkspaceId: requestUserWorkspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/jobs/run-evaluation-input.job.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/jobs/run-evaluation-input.job.ts index f9e8f2c8c8..524ff8bc05 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/jobs/run-evaluation-input.job.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/jobs/run-evaluation-input.job.ts @@ -6,6 +6,7 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service'; +import { AGENT_RUN_BASE_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; @@ -56,6 +57,7 @@ export class RunEvaluationInputJob { const executionResult = await this.aiAgentExecutorService.executeAgent({ agent, userPrompt: data.input, + baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT, workspaceId: data.workspaceId, userWorkspaceId: null, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const.ts new file mode 100644 index 0000000000..0848af5dda --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-run-base-system-prompt.const.ts @@ -0,0 +1,13 @@ +// Base system prompt for programmatic agent runs outside workflows (runAgent API, evaluations) +// NOTE: For user-facing chat, use CHAT_SYSTEM_PROMPTS from ai-chat/constants + +import { TOOL_USAGE_STRATEGY } from 'src/engine/metadata-modules/ai/ai-agent/constants/tool-usage-strategy.const'; + +export const AGENT_RUN_BASE_SYSTEM_PROMPT = `You are an AI agent in Twenty CRM, invoked programmatically to complete a request. + +${TOOL_USAGE_STRATEGY} + +Response: +- Your response is returned to the caller and may be shown directly to a person or processed by software +- Answer the request completely and directly +`; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts deleted file mode 100644 index 8313f60714..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts +++ /dev/null @@ -1,44 +0,0 @@ -// System prompts for Workflow Agents (automated execution only) -// NOTE: For user-facing chat, use CHAT_SYSTEM_PROMPTS from ai-chat/constants - -export const WORKFLOW_SYSTEM_PROMPTS = { - // Core workflow execution behavior - BASE: `You are executing as part of a workflow automation in Twenty CRM. - -Tool usage strategy: -- Chain multiple tools to solve complex tasks -- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, \`upsert_many_*\`, etc.) over looping single-item calls -- Use \`upsert_many_*\` instead of \`update_many_*\` when records have different data to set individually, or when some records may not exist yet -- If a tool fails, try alternative approaches -- Use results from one tool to inform the next -- Don't give up after first failure - be persistent - -Context: -- Your output may be used by downstream workflow nodes -- Be thorough and include all relevant data -- Focus on completing the task efficiently - -Permissions: -- Only perform actions your role allows`, - - // Structured output generation for workflow data passing - OUTPUT_GENERATOR: `You are a structured output generator for a workflow system. Your role is to convert the provided execution results into a structured format according to a specific schema. - -Context: Before this call, the system executed generateText with tools to perform any required actions and gather information. The execution results you receive include both the AI agent's analysis and any tool outputs from database operations, HTTP requests, data retrieval, or other actions. - -Your responsibilities: -1. Analyze the execution results from the AI agent (including any tool outputs) -2. Extract relevant information and data points from both text responses and tool results -3. Structure the data according to the provided schema -4. Ensure all required fields are populated with appropriate values -5. Handle missing or unclear data gracefully by providing reasonable defaults or null values -6. Maintain data integrity and consistency - -Guidelines: -- Focus on extracting and structuring the most relevant information -- If the execution results contain tool outputs (including HTTP requests), incorporate that data appropriately -- If certain schema fields cannot be populated from the results, use null or appropriate default values -- Preserve the context and meaning from the original execution results -- Ensure the output is clean, well-formatted, and ready for workflow consumption -- Pay special attention to any data returned from tool executions (database queries, HTTP requests, record creation, etc.)`, -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/structured-output-system-prompt.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/structured-output-system-prompt.const.ts new file mode 100644 index 0000000000..48627d6ef1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/structured-output-system-prompt.const.ts @@ -0,0 +1,19 @@ +export const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `You are a structured output generator. Your role is to convert the provided execution results into a structured format according to a specific schema. + +Context: Before this call, the system executed generateText with tools to perform any required actions and gather information. The execution results you receive include both the AI agent's analysis and any tool outputs from database operations, HTTP requests, data retrieval, or other actions. + +Your responsibilities: +1. Analyze the execution results from the AI agent (including any tool outputs) +2. Extract relevant information and data points from both text responses and tool results +3. Structure the data according to the provided schema +4. Ensure all required fields are populated with appropriate values +5. Handle missing or unclear data gracefully by providing reasonable defaults or null values +6. Maintain data integrity and consistency + +Guidelines: +- Focus on extracting and structuring the most relevant information +- If the execution results contain tool outputs (including HTTP requests), incorporate that data appropriately +- If certain schema fields cannot be populated from the results, use null or appropriate default values +- Preserve the context and meaning from the original execution results +- Ensure the output is clean, well-formatted, and ready for the caller to consume +- Pay special attention to any data returned from tool executions (database queries, HTTP requests, record creation, etc.)`; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/tool-usage-strategy.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/tool-usage-strategy.const.ts new file mode 100644 index 0000000000..97e9b7a192 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/tool-usage-strategy.const.ts @@ -0,0 +1,7 @@ +export const TOOL_USAGE_STRATEGY = `Tool usage strategy: +- Chain multiple tools to solve complex tasks +- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, \`upsert_many_*\`, etc.) over looping single-item calls +- Use \`upsert_many_*\` instead of \`update_many_*\` when records have different data to set individually, or when some records may not exist yet +- If a tool fails, try alternative approaches +- Use results from one tool to inform the next +- Don't give up after first failure - be persistent`; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/workflow-base-system-prompt.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/workflow-base-system-prompt.const.ts new file mode 100644 index 0000000000..908d2db4c5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/workflow-base-system-prompt.const.ts @@ -0,0 +1,13 @@ +import { TOOL_USAGE_STRATEGY } from 'src/engine/metadata-modules/ai/ai-agent/constants/tool-usage-strategy.const'; + +export const WORKFLOW_BASE_SYSTEM_PROMPT = `You are executing as part of a workflow automation in Twenty CRM. + +${TOOL_USAGE_STRATEGY} + +Context: +- Your output may be used by downstream workflow nodes +- Be thorough and include all relevant data +- Focus on completing the task efficiently + +Permissions: +- Only perform actions your role allows`; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts index 791c495050..055d27d265 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts @@ -7,6 +7,7 @@ import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/inte import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum'; import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service'; import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type'; +import { WORKFLOW_BASE_SYSTEM_PROMPT } from 'src/engine/metadata-modules/ai/ai-agent/constants/workflow-base-system-prompt.const'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; @@ -84,6 +85,7 @@ export class AiAgentWorkflowAction implements WorkflowAction { const executionResult = await this.aiAgentExecutionService.executeAgent({ agent, userPrompt: resolveInput(prompt, context) as string, + baseSystemPrompt: WORKFLOW_BASE_SYSTEM_PROMPT, actorContext: executionContext.isActingOnBehalfOfUser ? executionContext.initiator : undefined,