fix(ai-agent): use a non-workflow base system prompt for programmatic agent runs (#23394)
`AgentAsyncExecutorService` hardcoded `WORKFLOW_SYSTEM_PROMPTS.BASE`, so every caller was told "You are executing as part of a workflow automation" and "your output may be used by downstream workflow nodes". That is only true for the workflow AI-agent action. The `runAgent` API (used by apps such as the call recorder) and agent evaluations got the same framing, which does not describe how they run or where their output goes. The executor no longer asserts its own execution context: `executeAgent` now takes a required `baseSystemPrompt` and each caller supplies its own. - Workflow AI-agent action passes `WORKFLOW_SYSTEM_PROMPTS.BASE` (unchanged behavior) - `runAgent` and evaluations pass the new `AGENT_RUN_BASE_SYSTEM_PROMPT` The param is required rather than defaulted so every call site states its context and no future caller silently inherits the wrong one. Prompt constants are also split one export per file, with the shared tool-usage guidance extracted into `TOOL_USAGE_STRATEGY` so both bases compose it. No GraphQL schema, SDK, or database changes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23394?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+70
@@ -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<ReturnType<typeof generateText>>)
|
||||
.mockResolvedValueOnce({
|
||||
text: '',
|
||||
steps: [],
|
||||
usage: emptyUsage,
|
||||
output: { summary: 'done' },
|
||||
} as unknown as Awaited<ReturnType<typeof generateText>>);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
|
||||
+15
@@ -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' },
|
||||
|
||||
+9
-6
@@ -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:
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
});
|
||||
|
||||
+13
@@ -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
|
||||
`;
|
||||
-44
@@ -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.)`,
|
||||
};
|
||||
+19
@@ -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.)`;
|
||||
+7
@@ -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`;
|
||||
+13
@@ -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`;
|
||||
+2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user