From e518f03031bd0529084c3362e1ece28b04972d0b Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Thu, 6 Nov 2025 16:58:00 +0530
Subject: [PATCH] Fix: AI Agent tool errors and relation field handling
(#15668)
### Problems Fixed
1. **Tool execution errors broke conversations**
- Failed tool executions showed "Processing..." indefinitely instead of
error messages
- Tool errors with `input: null` caused subsequent messages to fail with
`Missing required parameter: 'input[X].arguments'`
2. **Relation fields not saved in AI Agent**
- AI Agent couldn't save relation fields (e.g., `companyId`) when
creating/upserting records
- Join column names weren't recognized during field validation
### Solutions
**Tool Error Handling:**
- Display error messages in UI with expandable error details
- Ensure tool parts always have valid `input` field (`input:
part.toolInput ?? {}`)
- Refactored `ToolStepRenderer` to accept complete `toolPart` object
**Relation Field Support:**
- Updated field validation in `create-record.service.ts` and
`upsert-record.service.ts`
- Check both `fieldIdByName` and `fieldIdByJoinColumnName` mappings
### Changes
- `packages/twenty-front/src/modules/ai/`
- `ToolStepRenderer.tsx` - Error state handling
- `AIChatAssistantMessageRenderer.tsx` - Pass complete toolPart
- `mapDBPartToUIMessagePart.ts` - Prevent null tool input
- `packages/twenty-server/src/engine/core-modules/record-crud/services/`
- `create-record.service.ts` - Add join column validation
- `upsert-record.service.ts` - Add join column validation
---
.../AIChatAssistantMessageRenderer.tsx | 10 +-
.../ai/components/ToolStepRenderer.tsx | 92 ++++++++++---------
.../ai/utils/mapDBPartToUIMessagePart.ts | 2 +-
.../services/create-record.service.ts | 8 +-
.../services/upsert-record.service.ts | 8 +-
5 files changed, 62 insertions(+), 58 deletions(-)
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx
index 5c5680924f..d7586bc0de 100644
--- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx
@@ -82,15 +82,7 @@ export const AIChatAssistantMessageRenderer = ({
default:
{
if (isToolUIPart(part)) {
- const { output, input, type } = part;
- return (
-
- );
+ return ;
}
}
return null;
diff --git a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx
index a954e7d07f..5892d5c250 100644
--- a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx
+++ b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx
@@ -3,11 +3,10 @@ import styled from '@emotion/styled';
import { useState } from 'react';
import { IconChevronDown, IconChevronUp } from 'twenty-ui/display';
-import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { JsonTree } from 'twenty-ui/json-visualizer';
+import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { ShimmeringText } from '@/ai/components/ShimmeringText';
-import { type ToolInput } from '@/ai/types/ToolInput';
import { getToolIcon } from '@/ai/utils/getToolIcon';
import { getToolDisplayMessage } from '@/ai/utils/getWebSearchToolDisplayMessage';
import { useLingui } from '@lingui/react/macro';
@@ -96,26 +95,22 @@ const StyledJsonContainer = styled.div`
type TabType = 'output' | 'input';
-export const ToolStepRenderer = ({
- input,
- output,
- toolName,
-}: {
- input: ToolInput;
- output: ToolUIPart['output'];
- toolName: string;
-}) => {
+export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
const { t } = useLingui();
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const [isExpanded, setIsExpanded] = useState(false);
const [activeTab, setActiveTab] = useState('output');
- const isExpandable = isDefined(output);
+ const { input, output, type, errorText } = toolPart;
+ const toolName = type.split('-')[1];
+
+ const hasError = isDefined(errorText);
+ const isExpandable = isDefined(output) || hasError;
const isTwoFirstDepths = ({ depth }: { depth: number }) => depth < 2;
- if (!output) {
+ if (!output && !hasError) {
return (
@@ -129,11 +124,12 @@ export const ToolStepRenderer = ({
);
}
- const displayMessage =
- output &&
- typeof output === 'object' &&
- 'message' in output &&
- typeof output.message === 'string'
+ const displayMessage = hasError
+ ? 'Tool execution failed'
+ : output &&
+ typeof output === 'object' &&
+ 'message' in output &&
+ typeof output.message === 'string'
? output.message
: getToolDisplayMessage(input, toolName, true);
@@ -165,33 +161,41 @@ export const ToolStepRenderer = ({
{isExpandable && (
-
- setActiveTab('output')}
- >
- Output
-
- setActiveTab('input')}
- >
- Input
-
-
+ {hasError ? (
+ {errorText}
+ ) : (
+ <>
+
+ setActiveTab('output')}
+ >
+ Output
+
+ setActiveTab('input')}
+ >
+ Input
+
+
-
-
-
+
+
+
+ >
+ )}
)}
diff --git a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts
index 72e702c5eb..f3414bebd0 100644
--- a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts
+++ b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts
@@ -59,7 +59,7 @@ export const mapDBPartToUIMessagePart = (
return {
type: part.type as `tool-${string}`,
toolCallId: part.toolCallId!,
- input: part.toolInput,
+ input: part.toolInput ?? {},
output: part.toolOutput,
errorText: part.errorMessage!,
state: part.state,
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts
index 12cdfd9315..bcbf9d9393 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts
@@ -73,8 +73,12 @@ export class CreateRecordService {
});
const validObjectRecord = Object.fromEntries(
- Object.entries(objectRecord).filter(([key]) =>
- isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]),
+ Object.entries(objectRecord).filter(
+ ([key]) =>
+ isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]) ||
+ isDefined(
+ objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[key],
+ ),
),
);
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
index 466b58b3d5..4b385cd534 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
@@ -92,9 +92,13 @@ export class UpsertRecordService {
});
const uniqueFieldsToUpdate = fieldsToUpdateArray
- .map((field) => objectMetadataItemWithFieldsMaps.fieldIdByName[field])
+ .map(
+ (field) =>
+ objectMetadataItemWithFieldsMaps.fieldIdByName[field] ||
+ objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[field],
+ )
.map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId])
- .filter((field) => field.isUnique || field.name === 'id');
+ .filter((field) => field && (field.isUnique || field.name === 'id'));
const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap(
(field) => {