[breaking-change] fix(server): return runAgent execution failures as result errors (#23390)

## Summary

- When `executeAgent` throws, `runAgent` now returns `{ success: false,
error }` instead of a GraphQL exception
- Callers (workflows, Slack assistant, etc.) can surface the failure to
users instead of hanging or failing opaquely

Run agent exception response error format breaking-change, errors moved
from the GraphQL error channel into the response payload.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23390?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:
Abdul Rahman
2026-07-29 00:46:57 +05:30
committed by GitHub
parent 5b10869a2b
commit 965e033f3f
2 changed files with 48 additions and 15 deletions
@@ -129,6 +129,24 @@ describe('AgentRunService', () => {
});
});
it('returns a generic error result when agent execution throws instead of bubbling GraphQL errors', async () => {
agentAsyncExecutorService.executeAgent.mockRejectedValue(
new Error('Failed to process successful response'),
);
const result = await service.run({
workspace,
requestUserWorkspaceId: 'user-workspace-1',
input,
});
expect(result).toEqual({
result: null,
error: 'Agent execution failed.',
success: false,
});
});
it('throws when no agent matches the identifier', async () => {
agentRepository.findOne.mockResolvedValue(null);
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import {
type RunAgentInput,
@@ -17,6 +17,8 @@ import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scope
@Injectable()
export class AgentRunService {
private readonly logger = new Logger(AgentRunService.name);
constructor(
private readonly agentAsyncExecutorService: AgentAsyncExecutorService,
private readonly applicationService: ApplicationService,
@@ -61,25 +63,38 @@ export class AgentRunService {
application,
};
const { result, hasNoMoreAvailableCredits } =
await this.agentAsyncExecutorService.executeAgent({
agent,
userPrompt: input.prompt,
baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT,
authContext,
workspaceId: workspace.id,
userWorkspaceId: requestUserWorkspaceId,
operationType: UsageOperationType.AI_WORKFLOW_TOKEN,
});
try {
const { result, hasNoMoreAvailableCredits } =
await this.agentAsyncExecutorService.executeAgent({
agent,
userPrompt: input.prompt,
baseSystemPrompt: AGENT_RUN_BASE_SYSTEM_PROMPT,
authContext,
workspaceId: workspace.id,
userWorkspaceId: requestUserWorkspaceId,
operationType: UsageOperationType.AI_WORKFLOW_TOKEN,
});
if (hasNoMoreAvailableCredits) {
return {
result: null,
error: 'AI agent stopped: no more available credits.',
success: false,
};
}
return { result, error: null, success: true };
} catch (error) {
this.logger.error(
`Agent execution failed for ${input.agentUniversalIdentifier}`,
error instanceof Error ? error.stack : error,
);
if (hasNoMoreAvailableCredits) {
return {
result: null,
error: 'AI agent stopped: no more available credits.',
error: 'Agent execution failed.',
success: false,
};
}
return { result, error: null, success: true };
}
}