AI SDK v5 migration (#14549)

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman 2025-09-23 01:43:43 +05:30 committed by GitHub
parent e8121919bd
commit 216d72b5d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 1347 additions and 841 deletions

View file

@ -257,13 +257,16 @@ jobs:
run: |
git stash
git checkout origin/main
git clean -fd
git reset --hard
git clean -xfd -ff
rm -rf node_modules packages/*/node_modules packages/*/dist dist .nx/cache
- name: Install dependencies for main branch
uses: ./.github/workflows/actions/yarn-install
- name: Build main branch dependencies
run: |
npx nx reset
npx nx build twenty-shared
npx nx build twenty-emails
@ -272,6 +275,7 @@ jobs:
- name: Setup main branch database
run: |
npx nx reset:env twenty-server
# Function to set or update environment variable
set_env_var() {
local var_name="$1"
@ -287,6 +291,9 @@ jobs:
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch"
set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod

View file

@ -1,7 +1,8 @@
import { ErrorStepRenderer } from '@/ai/components/ErrorStepRenderer';
import { ReasoningSummaryDisplay } from '@/ai/components/ReasoningSummaryDisplay';
import { ToolStepRenderer } from '@/ai/components/ToolStepRenderer';
import type { ParsedStep } from '@/ai/types/streamTypes';
import type { ParsedStep } from '@/ai/types/ParsedStep';
import { hasStructuredStreamData } from '@/ai/utils/hasStructuredStreamData';
import { parseStream } from '@/ai/utils/parseStream';
import { IconDotsVertical } from 'twenty-ui/display';
@ -64,25 +65,15 @@ export const AIChatAssistantMessageRenderer = ({
streamData: string;
}) => {
const agentStreamingMessage = useRecoilValue(agentStreamingMessageState);
const isStreaming =
Boolean(agentStreamingMessage) && streamData === agentStreamingMessage;
const isStreaming = streamData === agentStreamingMessage;
if (!streamData) {
return <LoadingDotsIcon />;
}
const isPlainString =
!streamData.includes('\n') ||
!streamData.split('\n').some((line) => {
try {
JSON.parse(line);
return true;
} catch {
return false;
}
});
const hasStructuredData = hasStructuredStreamData(streamData);
if (isPlainString) {
if (!hasStructuredData) {
return <LazyMarkdownRenderer text={streamData} />;
}

View file

@ -1,6 +1,7 @@
import { extractErrorMessage } from '@/ai/utils/extractErrorMessage';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconAlertCircle } from 'twenty-ui/display';
const StyledContainer = styled.div`
@ -53,7 +54,7 @@ export const ErrorStepRenderer = ({
<IconAlertCircle size={theme.icon.size.md} />
</StyledIconContainer>
<StyledContent>
<StyledTitle>Error</StyledTitle>
<StyledTitle>{t`Error`}</StyledTitle>
<StyledMessage>{errorMessage}</StyledMessage>
</StyledContent>
</StyledContainer>

View file

@ -6,13 +6,13 @@ import { IconChevronDown, IconChevronUp } from 'twenty-ui/display';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { ShimmeringText } from '@/ai/components/ShimmeringText';
import { getToolIcon } from '@/ai/utils/getToolIcon';
import type {
ToolCallEvent,
ToolEvent,
ToolResultEvent,
} from '@/ai/types/streamTypes';
import { extractErrorMessage } from '@/ai/utils/extractErrorMessage';
import { getToolIcon } from '@/ai/utils/getToolIcon';
} from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
@ -74,32 +74,27 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => {
const theme = useTheme();
const [isExpanded, setIsExpanded] = useState(false);
const toolCall = events[0] as ToolCallEvent | undefined;
const toolResult = events.find(
const toolCallEvent = events[0] as ToolCallEvent | undefined;
const toolResultEvent = events.find(
(event): event is ToolResultEvent => event.type === 'tool-result',
);
if (!toolCall) {
if (!toolCallEvent) {
return null;
}
const toolOutput = toolResult?.result as ToolResultEvent['result'];
const isStandardizedFormat =
toolOutput && typeof toolOutput === 'object' && 'success' in toolOutput;
const toolOutput =
toolResultEvent?.output?.error ?? toolResultEvent?.output?.result;
const hasResult = isStandardizedFormat
? Boolean(toolOutput.result)
: Boolean(toolResult?.result);
const hasError = isStandardizedFormat ? Boolean(toolOutput.error) : false;
const isExpandable = hasResult || hasError;
const isExpandable = isDefined(toolOutput);
if (!toolResult) {
if (!toolResultEvent) {
return (
<StyledContainer>
<StyledLoadingContainer>
<ShimmeringText>
<StyledDisplayMessage>
{toolCall.args.loadingMessage}
{toolCallEvent.input.loadingMessage}
</StyledDisplayMessage>
</ShimmeringText>
</StyledLoadingContainer>
@ -108,13 +103,13 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => {
}
const displayMessage =
toolResult?.result &&
typeof toolResult.result === 'object' &&
'message' in toolResult.result
? (toolResult.result as { message: string }).message
toolResultEvent?.output &&
typeof toolResultEvent.output === 'object' &&
'message' in toolResultEvent.output
? (toolResultEvent.output as { message: string }).message
: undefined;
const ToolIcon = getToolIcon(toolCall.toolName);
const ToolIcon = getToolIcon(toolCallEvent.toolName);
return (
<StyledContainer>
@ -137,20 +132,7 @@ export const ToolStepRenderer = ({ events }: { events: ToolEvent[] }) => {
{isExpandable && (
<AnimatedExpandableContainer isExpanded={isExpanded}>
<StyledContentContainer>
{isStandardizedFormat ? (
<>
{hasError && <div>{extractErrorMessage(toolOutput.error)}</div>}
{hasResult && (
<div>
<StyledPre>
{JSON.stringify(toolOutput.result, null, 2)}
</StyledPre>
</div>
)}
</>
) : toolResult?.result ? (
JSON.stringify(toolResult.result, null, 2)
) : undefined}
<StyledPre>{JSON.stringify(toolOutput, null, 2)}</StyledPre>
</StyledContentContainer>
</AnimatedExpandableContainer>
)}

View file

@ -0,0 +1,7 @@
import type { ToolEvent } from 'twenty-shared/ai';
export type ParsedStep =
| { type: 'tool'; events: ToolEvent[] }
| { type: 'reasoning'; content: string; isThinking: boolean }
| { type: 'text'; content: string }
| { type: 'error'; message: string; error?: unknown };

View file

@ -1,36 +0,0 @@
export type ToolCallEvent = {
type: 'tool-call';
toolCallId: string;
toolName: string;
args: {
loadingMessage: string;
input: unknown;
};
};
export type ToolResultEvent = {
type: 'tool-result';
toolCallId: string;
toolName: string;
result: {
success: boolean;
result?: unknown;
error?: string;
message: string;
};
message: string;
};
export type ToolEvent = ToolCallEvent | ToolResultEvent;
export type ErrorEvent = {
type: 'error';
message: string;
error?: unknown;
};
export type ParsedStep =
| { type: 'tool'; events: ToolEvent[] }
| { type: 'reasoning'; content: string; isThinking: boolean }
| { type: 'text'; content: string }
| { type: 'error'; message: string; error?: unknown };

View file

@ -46,7 +46,7 @@ describe('parseStream', () => {
type: 'tool-result',
toolCallId: 'call-123',
toolName: 'send_email',
result: { sucess: true, result: 'Email sent', message: 'Success' },
result: { success: true, result: 'Email sent', message: 'Success' },
message: 'Email sent successfully',
}),
].join('\n');
@ -67,7 +67,7 @@ describe('parseStream', () => {
type: 'tool-result',
toolCallId: 'call-123',
toolName: 'send_email',
result: { sucess: true, result: 'Email sent', message: 'Success' },
result: { success: true, result: 'Email sent', message: 'Success' },
message: 'Email sent successfully',
},
],
@ -80,7 +80,7 @@ describe('parseStream', () => {
toolCallId: 'call-456',
toolName: 'http_request',
result: {
sucess: true,
success: true,
result: 'Response received',
message: 'Success',
},
@ -98,7 +98,7 @@ describe('parseStream', () => {
toolCallId: 'call-456',
toolName: 'http_request',
result: {
sucess: true,
success: true,
result: 'Response received',
message: 'Success',
},
@ -112,15 +112,16 @@ describe('parseStream', () => {
describe('reasoning events', () => {
it('should parse reasoning events correctly', () => {
const streamText = [
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({
type: 'reasoning',
textDelta: 'Let me think about this...',
type: 'reasoning-delta',
text: 'Let me think about this...',
}),
JSON.stringify({
type: 'reasoning',
textDelta: ' I need to consider the options.',
type: 'reasoning-delta',
text: ' I need to consider the options.',
}),
JSON.stringify({ type: 'reasoning-signature' }),
JSON.stringify({ type: 'reasoning-end' }),
].join('\n');
const result = parseStream(streamText);
@ -133,11 +134,14 @@ describe('parseStream', () => {
});
});
it('should handle reasoning without signature as thinking', () => {
const streamText = JSON.stringify({
type: 'reasoning',
textDelta: 'Still thinking...',
});
it('should handle reasoning without end as thinking', () => {
const streamText = [
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({
type: 'reasoning-delta',
text: 'Still thinking...',
}),
].join('\n');
const result = parseStream(streamText);
@ -151,9 +155,10 @@ describe('parseStream', () => {
it('should concatenate multiple reasoning deltas', () => {
const streamText = [
JSON.stringify({ type: 'reasoning', textDelta: 'First part' }),
JSON.stringify({ type: 'reasoning', textDelta: ' second part' }),
JSON.stringify({ type: 'reasoning', textDelta: ' third part' }),
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({ type: 'reasoning-delta', text: 'First part' }),
JSON.stringify({ type: 'reasoning-delta', text: ' second part' }),
JSON.stringify({ type: 'reasoning-delta', text: ' third part' }),
].join('\n');
const result = parseStream(streamText);
@ -170,8 +175,8 @@ describe('parseStream', () => {
describe('text-delta events', () => {
it('should parse text-delta events correctly', () => {
const streamText = [
JSON.stringify({ type: 'text-delta', textDelta: 'Hello, ' }),
JSON.stringify({ type: 'text-delta', textDelta: 'world!' }),
JSON.stringify({ type: 'text-delta', text: 'Hello, ' }),
JSON.stringify({ type: 'text-delta', text: 'world!' }),
].join('\n');
const result = parseStream(streamText);
@ -183,8 +188,8 @@ describe('parseStream', () => {
});
});
it('should handle empty textDelta', () => {
const streamText = JSON.stringify({ type: 'text-delta', textDelta: '' });
it('should handle empty text', () => {
const streamText = JSON.stringify({ type: 'text-delta', text: '' });
const result = parseStream(streamText);
@ -249,7 +254,7 @@ describe('parseStream', () => {
describe('step-finish events', () => {
it('should flush current text block on step-finish', () => {
const streamText = [
JSON.stringify({ type: 'text-delta', textDelta: 'Some text' }),
JSON.stringify({ type: 'text-delta', text: 'Some text' }),
JSON.stringify({ type: 'step-finish' }),
].join('\n');
@ -264,7 +269,8 @@ describe('parseStream', () => {
it('should mark reasoning as not thinking on step-finish', () => {
const streamText = [
JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }),
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }),
JSON.stringify({ type: 'step-finish' }),
].join('\n');
@ -282,22 +288,23 @@ describe('parseStream', () => {
describe('mixed events', () => {
it('should handle mixed event types correctly', () => {
const streamText = [
JSON.stringify({ type: 'text-delta', textDelta: 'Starting...' }),
JSON.stringify({ type: 'text-delta', text: 'Starting...' }),
JSON.stringify({
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'send_email',
args: { loadingMessage: 'Sending...', input: {} },
}),
JSON.stringify({ type: 'reasoning', textDelta: 'Let me think...' }),
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({ type: 'reasoning-delta', text: 'Let me think...' }),
JSON.stringify({
type: 'tool-result',
toolCallId: 'call-1',
toolName: 'send_email',
result: { sucess: true, message: 'Done' },
result: { success: true, message: 'Done' },
message: 'Email sent',
}),
JSON.stringify({ type: 'text-delta', textDelta: 'Finished!' }),
JSON.stringify({ type: 'text-delta', text: 'Finished!' }),
].join('\n');
const result = parseStream(streamText);
@ -317,7 +324,7 @@ describe('parseStream', () => {
type: 'tool-result',
toolCallId: 'call-1',
toolName: 'send_email',
result: { sucess: true, message: 'Done' },
result: { success: true, message: 'Done' },
message: 'Email sent',
},
],
@ -348,7 +355,7 @@ describe('parseStream', () => {
it('should skip invalid JSON lines', () => {
const streamText = [
'invalid json line',
JSON.stringify({ type: 'text-delta', textDelta: 'Valid content' }),
JSON.stringify({ type: 'text-delta', text: 'Valid content' }),
'another invalid line',
].join('\n');
@ -374,7 +381,7 @@ describe('parseStream', () => {
it('should flush remaining text block at end', () => {
const streamText = JSON.stringify({
type: 'text-delta',
textDelta: 'Unflushed content',
text: 'Unflushed content',
});
const result = parseStream(streamText);
@ -390,8 +397,9 @@ describe('parseStream', () => {
describe('text block transitions', () => {
it('should create new text block when switching from reasoning to text', () => {
const streamText = [
JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }),
JSON.stringify({ type: 'text-delta', textDelta: 'Speaking...' }),
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }),
JSON.stringify({ type: 'text-delta', text: 'Speaking...' }),
].join('\n');
const result = parseStream(streamText);
@ -410,8 +418,9 @@ describe('parseStream', () => {
it('should create new reasoning block when switching from text to reasoning', () => {
const streamText = [
JSON.stringify({ type: 'text-delta', textDelta: 'Speaking...' }),
JSON.stringify({ type: 'reasoning', textDelta: 'Thinking...' }),
JSON.stringify({ type: 'text-delta', text: 'Speaking...' }),
JSON.stringify({ type: 'reasoning-start' }),
JSON.stringify({ type: 'reasoning-delta', text: 'Thinking...' }),
].join('\n');
const result = parseStream(streamText);

View file

@ -1,29 +1,39 @@
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const extractErrorMessage = (error: unknown): string => {
if (typeof error === 'string') {
return error;
}
const isObjectWithMessage = (error: unknown): error is { message: string } => {
return (
isDefined(error) &&
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string'
);
};
if (!isDefined(error) || typeof error !== 'object') {
return 'An unexpected error occurred';
}
if ('message' in error && typeof error.message === 'string') {
return error.message;
}
if (
const isErrorWithNestedError = (
error: unknown,
): error is {
error: { message: string };
} => {
return (
isDefined(error) &&
typeof error === 'object' &&
'error' in error &&
isDefined(error.error) &&
typeof error.error === 'object' &&
'message' in error.error &&
typeof error.error.message === 'string'
) {
return error.error.message;
}
);
};
if (
const isDeepNestedError = (
error: unknown,
): error is {
data: { error: { message: string } };
} => {
return (
isDefined(error) &&
typeof error === 'object' &&
'data' in error &&
isDefined(error.data) &&
typeof error.data === 'object' &&
@ -32,9 +42,25 @@ export const extractErrorMessage = (error: unknown): string => {
typeof error.data.error === 'object' &&
'message' in error.data.error &&
typeof error.data.error.message === 'string'
) {
);
};
export const extractErrorMessage = (error: unknown): string => {
if (typeof error === 'string') {
return error;
}
if (isObjectWithMessage(error)) {
return error.message;
}
if (isErrorWithNestedError(error)) {
return error.error.message;
}
if (isDeepNestedError(error)) {
return error.data.error.message;
}
return 'An unexpected error occurred';
return t`An unexpected error occurred`;
};

View file

@ -0,0 +1,14 @@
export const hasStructuredStreamData = (data: string): boolean => {
if (!data.includes('\n')) {
return false;
}
return data.split('\n').some((line) => {
try {
JSON.parse(line);
return true;
} catch {
return false;
}
});
};

View file

@ -1,19 +1,130 @@
import { isDefined } from 'twenty-shared/utils';
import {
type ParsedStep,
parseStreamLine,
splitStreamIntoLines,
type ErrorEvent,
type ReasoningDeltaEvent,
type TextBlock,
type TextDeltaEvent,
type ToolCallEvent,
type ToolEvent,
type ToolResultEvent,
} from '@/ai/types/streamTypes';
} from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
type TextBlock =
| { type: 'reasoning'; content: string; isThinking: boolean }
| { type: 'text'; content: string }
| null;
import type { ParsedStep } from '@/ai/types/ParsedStep';
const handleToolCall = (
event: ToolCallEvent,
output: ParsedStep[],
flushTextBlock: () => void,
) => {
flushTextBlock();
output.push({
type: 'tool',
events: [event],
});
};
const handleToolResult = (
event: ToolResultEvent,
output: ParsedStep[],
flushTextBlock: () => void,
) => {
flushTextBlock();
const toolEntry = output.find(
(item): item is { type: 'tool'; events: ToolEvent[] } =>
item.type === 'tool' &&
item.events.some(
(e) => e.type === 'tool-call' && e.toolCallId === event.toolCallId,
),
);
if (isDefined(toolEntry)) {
toolEntry.events.push(event);
} else {
output.push({
type: 'tool',
events: [event],
});
}
};
const handleReasoningStart = (flushTextBlock: () => void): TextBlock => {
flushTextBlock();
return {
type: 'reasoning',
content: '',
isThinking: true,
};
};
const handleReasoningDelta = (
event: ReasoningDeltaEvent,
currentTextBlock: TextBlock,
flushTextBlock: () => void,
): TextBlock => {
if (!currentTextBlock || currentTextBlock.type !== 'reasoning') {
flushTextBlock();
return {
type: 'reasoning',
content: event.text || '',
isThinking: true,
};
}
currentTextBlock.content += event.text || '';
return currentTextBlock;
};
const handleReasoningEnd = (currentTextBlock: TextBlock): TextBlock => {
if (currentTextBlock?.type === 'reasoning') {
return {
...currentTextBlock,
isThinking: false,
};
}
return currentTextBlock;
};
const handleTextDelta = (
event: TextDeltaEvent,
currentTextBlock: TextBlock,
flushTextBlock: () => void,
): TextBlock => {
if (!currentTextBlock || currentTextBlock.type !== 'text') {
flushTextBlock();
return { type: 'text', content: event.text || '' };
}
currentTextBlock.content += event.text || '';
return currentTextBlock;
};
const handleStepFinish = (
currentTextBlock: TextBlock,
flushTextBlock: () => void,
): TextBlock => {
if (currentTextBlock?.type === 'reasoning') {
currentTextBlock.isThinking = false;
}
flushTextBlock();
return null;
};
const handleError = (
event: ErrorEvent,
output: ParsedStep[],
flushTextBlock: () => void,
) => {
flushTextBlock();
output.push({
type: 'error',
message: event.message || 'An error occurred',
error: event.error,
});
};
export const parseStream = (streamText: string): ParsedStep[] => {
const lines = streamText.trim().split('\n');
const lines = splitStreamIntoLines(streamText);
const output: ParsedStep[] = [];
let currentTextBlock: TextBlock = null;
@ -25,100 +136,50 @@ export const parseStream = (streamText: string): ParsedStep[] => {
};
for (const line of lines) {
let event;
try {
event = JSON.parse(line);
} catch {
const event = parseStreamLine(line);
if (!event) {
continue;
}
switch (event.type) {
case 'tool-call':
flushTextBlock();
output.push({
type: 'tool',
events: [
{
type: 'tool-call',
toolCallId: event.toolCallId,
toolName: event.toolName,
args: event.args,
},
] as ToolEvent[],
});
handleToolCall(event, output, flushTextBlock);
break;
case 'tool-result': {
flushTextBlock();
case 'tool-result':
handleToolResult(event, output, flushTextBlock);
break;
const toolEntry = output.find(
(item): item is { type: 'tool'; events: ToolEvent[] } =>
item.type === 'tool' &&
item.events.some(
(e) =>
e.type === 'tool-call' && e.toolCallId === event.toolCallId,
),
case 'reasoning-start':
currentTextBlock = handleReasoningStart(flushTextBlock);
break;
case 'reasoning-delta':
currentTextBlock = handleReasoningDelta(
event,
currentTextBlock,
flushTextBlock,
);
const resultEvent: ToolResultEvent = {
type: 'tool-result',
toolCallId: event.toolCallId,
toolName: event.toolName,
result: event.result,
message: event.message,
};
if (isDefined(toolEntry)) {
toolEntry.events.push(resultEvent);
} else {
output.push({
type: 'tool',
events: [resultEvent],
});
}
break;
}
case 'reasoning':
if (!currentTextBlock || currentTextBlock.type !== 'reasoning') {
flushTextBlock();
currentTextBlock = {
type: 'reasoning',
content: '',
isThinking: true,
};
}
currentTextBlock.content += event.textDelta || '';
case 'reasoning-end':
currentTextBlock = handleReasoningEnd(currentTextBlock);
break;
case 'text-delta':
if (!currentTextBlock || currentTextBlock.type !== 'text') {
flushTextBlock();
currentTextBlock = { type: 'text', content: '' };
}
currentTextBlock.content += event.textDelta || '';
break;
case 'reasoning-signature':
if (currentTextBlock?.type === 'reasoning') {
currentTextBlock.isThinking = false;
}
currentTextBlock = handleTextDelta(
event,
currentTextBlock,
flushTextBlock,
);
break;
case 'step-finish':
if (currentTextBlock?.type === 'reasoning') {
currentTextBlock.isThinking = false;
}
flushTextBlock();
currentTextBlock = handleStepFinish(currentTextBlock, flushTextBlock);
break;
case 'error':
flushTextBlock();
output.push({
type: 'error',
message: event.message || 'An error occurred',
error: event.error,
});
handleError(event, output, flushTextBlock);
break;
}
}

View file

@ -1,6 +1,6 @@
import { type WorkflowRunState } from '@/workflow/types/Workflow';
import { workflowRunStateSchema } from '@/workflow/validation-schemas/workflowSchema';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunStateSchema } from 'twenty-shared/workflow';
import { type JsonValue } from 'type-fest';
export const orderWorkflowRunState = (value: JsonValue) => {

View file

@ -1,9 +1,9 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunSchema } from 'twenty-shared/workflow';
export const useWorkflowRun = ({
workflowRunId,

View file

@ -20,7 +20,7 @@ import {
type workflowTriggerSchema,
type workflowUpdateRecordActionSchema,
type workflowWebhookTriggerSchema,
} from 'twenty-shared/workflow';
} from '@/workflow/validation-schemas/workflowSchema';
import { type z } from 'zod';
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;

View file

@ -0,0 +1,390 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { StepStatus } from 'twenty-shared/workflow';
import { z } from 'zod';
export const objectRecordSchema = z.record(z.any());
export const baseWorkflowActionSettingsSchema = z.object({
input: z.object({}).passthrough(),
outputSchema: z.object({}).passthrough(),
errorHandlingOptions: z.object({
retryOnFailure: z.object({
value: z.boolean(),
}),
continueOnFailure: z.object({
value: z.boolean(),
}),
}),
});
export const baseWorkflowActionSchema = z.object({
id: z.string(),
name: z.string(),
valid: z.boolean(),
nextStepIds: z.array(z.string()).optional().nullable(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
});
export const baseTriggerSchema = z.object({
name: z.string().optional(),
type: z.string(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
nextStepIds: z.array(z.string()).optional().nullable(),
});
export const workflowCodeActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
serverlessFunctionId: z.string(),
serverlessFunctionVersion: z.string(),
serverlessFunctionInput: z.record(z.any()),
}),
});
export const workflowSendEmailActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
connectedAccountId: z.string(),
email: z.string(),
subject: z.string().optional(),
body: z.string().optional(),
}),
});
export const workflowCreateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
}),
});
export const workflowUpdateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
objectRecordId: z.string(),
fieldsToUpdate: z.array(z.string()),
}),
});
export const workflowDeleteRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecordId: z.string(),
}),
});
export const workflowFindRecordsActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
limit: z.number().optional(),
filter: z
.object({
recordFilterGroups: z.array(z.object({})).optional(),
recordFilters: z.array(z.object({})).optional(),
gqlOperationFilter: z.object({}).optional().nullable(),
})
.optional(),
}),
});
export const workflowFormActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.array(
z.object({
id: z.string(),
name: z.string(),
label: z.string(),
type: z.union([
z.literal(FieldMetadataType.TEXT),
z.literal(FieldMetadataType.NUMBER),
z.literal(FieldMetadataType.DATE),
z.literal(FieldMetadataType.SELECT),
z.literal('RECORD'),
]),
placeholder: z.string().optional(),
settings: z.record(z.any()).optional(),
value: z.any().optional(),
}),
),
});
export const workflowHttpRequestActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
url: z.string(),
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
headers: z.record(z.string()).optional(),
body: z
.record(
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
]),
)
.or(z.string())
.optional(),
}),
});
export const workflowAiAgentActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
agentId: z.string().optional(),
prompt: z.string().optional(),
}),
});
export const workflowFilterActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
stepFilterGroups: z.array(z.any()),
stepFilters: z.array(z.any()),
}),
});
export const workflowIteratorActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
items: z
.union([
z.array(
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.record(z.any()),
z.any(),
]),
),
z.string(),
])
.optional(),
initialLoopStepIds: z.array(z.string()).optional(),
}),
});
export const workflowEmptyActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({}),
});
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
settings: workflowCodeActionSettingsSchema,
});
export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('SEND_EMAIL'),
settings: workflowSendEmailActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
},
);
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
},
);
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
},
);
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FIND_RECORDS'),
settings: workflowFindRecordsActionSettingsSchema,
});
export const workflowFormActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FORM'),
settings: workflowFormActionSettingsSchema,
});
export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('HTTP_REQUEST'),
settings: workflowHttpRequestActionSettingsSchema,
});
export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('AI_AGENT'),
settings: workflowAiAgentActionSettingsSchema,
});
export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FILTER'),
settings: workflowFilterActionSettingsSchema,
});
export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('ITERATOR'),
settings: workflowIteratorActionSettingsSchema,
});
export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('EMPTY'),
settings: workflowEmptyActionSettingsSchema,
});
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema,
workflowFormActionSchema,
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
workflowIteratorActionSchema,
workflowEmptyActionSchema,
]);
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z.string(),
input: z.object({}).passthrough().optional(),
outputSchema: z.object({}).passthrough(),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
}),
});
export const workflowManualTriggerSchema = baseTriggerSchema
.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
})
.describe(
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
);
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
type: z.literal('CRON'),
settings: z.discriminatedUnion('type', [
z.object({
type: z.literal('DAYS'),
schedule: z.object({
day: z.number().min(1),
hour: z.number().min(0).max(23),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('HOURS'),
schedule: z.object({
hour: z.number().min(1),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('MINUTES'),
schedule: z.object({ minute: z.number().min(1) }),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('CUSTOM'),
pattern: z.string(),
outputSchema: z.object({}).passthrough(),
}),
]),
});
export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({
type: z.literal('WEBHOOK'),
settings: z.discriminatedUnion('httpMethod', [
z.object({
outputSchema: z.object({}).passthrough(),
httpMethod: z.literal('GET'),
authentication: z.literal('API_KEY').nullable(),
}),
z.object({
outputSchema: z.object({}).passthrough(),
httpMethod: z.literal('POST'),
expectedBody: z.object({}).passthrough(),
authentication: z.literal('API_KEY').nullable(),
}),
]),
});
export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
workflowWebhookTriggerSchema,
]);
export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus);
export const workflowRunStateStepInfoSchema = z.object({
result: z.any().optional(),
error: z.any().optional(),
status: workflowRunStepStatusSchema,
});
export const workflowRunStateStepInfosSchema = z.record(
workflowRunStateStepInfoSchema,
);
export const workflowRunStateSchema = z.object({
flow: z.object({
trigger: workflowTriggerSchema,
steps: z.array(workflowActionSchema),
}),
stepInfos: workflowRunStateStepInfosSchema,
workflowRunError: z.any().optional(),
});
export const workflowRunStatusSchema = z.enum([
'NOT_STARTED',
'RUNNING',
'COMPLETED',
'FAILED',
'ENQUEUED',
]);
export const workflowRunSchema = z
.object({
__typename: z.literal('WorkflowRun'),
id: z.string(),
workflowVersionId: z.string(),
workflowId: z.string(),
state: workflowRunStateSchema.nullable(),
status: workflowRunStatusSchema,
createdAt: z.string(),
deletedAt: z.string().nullable(),
endedAt: z.string().nullable(),
name: z.string(),
})
.passthrough();

View file

@ -3,13 +3,13 @@ import {
type WorkflowHttpRequestAction,
type WorkflowSendEmailAction,
} from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
import {
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
} from 'twenty-shared/workflow';
} from '@/workflow/validation-schemas/workflowSchema';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
jest.mock('../useActionIconColorOrThrow', () => ({

View file

@ -15,9 +15,10 @@
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/xai": "1.2.15",
"@ai-sdk/anthropic": "^2.0.17",
"@ai-sdk/openai": "^2.0.30",
"@ai-sdk/provider-utils": "^3.0.9",
"@ai-sdk/xai": "^2.0.19",
"@aws-sdk/client-lambda": "3.825.0",
"@aws-sdk/client-s3": "3.825.0",
"@aws-sdk/client-sts": "3.825.0",
@ -77,7 +78,7 @@
"@sentry/profiling-node": "9.26.0",
"@sniptt/guards": "0.2.0",
"addressparser": "1.0.1",
"ai": "^4.3.16",
"ai": "^5.0.44",
"apollo-server-core": "3.13.0",
"archiver": "7.0.1",
"axios": "1.10.0",
@ -186,7 +187,7 @@
"unzipper": "^0.12.3",
"uuid": "9.0.1",
"vite-tsconfig-paths": "4.3.2",
"zod": "3.23.8",
"zod": "^4.1.11",
"zod-to-json-schema": "^3.23.1"
},
"devDependencies": {

View file

@ -1,8 +1,12 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { openai } from '@ai-sdk/openai';
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AiService } from 'src/engine/core-modules/ai/services/ai.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AiController } from './ai.controller';
@ -11,6 +15,7 @@ describe('AiController', () => {
let aiService: jest.Mocked<AiService>;
let featureFlagService: jest.Mocked<FeatureFlagService>;
let aiBillingService: jest.Mocked<AIBillingService>;
let aiModelRegistryService: jest.Mocked<AiModelRegistryService>;
beforeEach(async () => {
const mockAiService = {
@ -26,6 +31,14 @@ describe('AiController', () => {
calculateAndBillUsage: jest.fn(),
};
const mockAiModelRegistryService = {
getDefaultModel: jest.fn().mockReturnValue({
modelId: 'gpt-4o',
provider: ModelProvider.OPENAI,
model: openai('gpt-4o'),
}),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [AiController],
providers: [
@ -41,6 +54,10 @@ describe('AiController', () => {
provide: AIBillingService,
useValue: mockAIBillingService,
},
{
provide: AiModelRegistryService,
useValue: mockAiModelRegistryService,
},
],
}).compile();
@ -48,6 +65,7 @@ describe('AiController', () => {
aiService = module.get(AiService);
featureFlagService = module.get(FeatureFlagService);
aiBillingService = module.get(AIBillingService);
aiModelRegistryService = module.get(AiModelRegistryService);
});
it('should be defined', () => {
@ -61,7 +79,7 @@ describe('AiController', () => {
const mockRequest = {
messages: [{ role: 'user' as const, content: 'Hello' }],
temperature: 0.7,
maxTokens: 100,
maxOutputTokens: 100,
};
const mockRes = {
@ -70,19 +88,23 @@ describe('AiController', () => {
end: jest.fn(),
} as any;
const mockModel = { modelId: 'gpt-4o' } as any;
const mockModel = openai('gpt-4o');
aiService.getModel.mockReturnValue(mockModel);
aiModelRegistryService.getDefaultModel.mockReturnValue({
modelId: 'gpt-4o',
provider: ModelProvider.OPENAI,
model: mockModel,
});
const mockUsage = {
promptTokens: 10,
completionTokens: 20,
inputTokens: 10,
outputTokens: 20,
totalTokens: 30,
};
const mockStreamTextResult = {
usage: Promise.resolve(mockUsage),
pipeDataStreamToResponse: jest.fn(),
pipeUIMessageStreamToResponse: jest.fn(),
};
aiService.streamText.mockReturnValue(mockStreamTextResult as any);
@ -96,12 +118,12 @@ describe('AiController', () => {
messages: mockRequest.messages,
options: {
temperature: 0.7,
maxTokens: 100,
maxOutputTokens: 100,
model: mockModel,
},
});
expect(
mockStreamTextResult.pipeDataStreamToResponse,
mockStreamTextResult.pipeUIMessageStreamToResponse,
).toHaveBeenCalledWith(mockRes);
expect(aiBillingService.calculateAndBillUsage).toHaveBeenCalledWith(
mockModel.modelId,
@ -131,7 +153,11 @@ describe('AiController', () => {
const mockRes = {} as any;
aiService.getModel.mockReturnValue({ modelId: 'gpt-4o' } as any);
aiModelRegistryService.getDefaultModel.mockReturnValue({
modelId: 'gpt-4o',
provider: ModelProvider.OPENAI,
model: openai('gpt-4o'),
});
aiService.streamText.mockImplementation(() => {
throw new Error('Service error');
});

View file

@ -8,21 +8,22 @@ import {
UseGuards,
} from '@nestjs/common';
import { type CoreMessage } from 'ai';
import { type ModelMessage } from 'ai';
import { Response } from 'express';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AiService } from 'src/engine/core-modules/ai/services/ai.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
export interface ChatRequest {
messages: CoreMessage[];
messages: ModelMessage[];
temperature?: number;
maxTokens?: number;
maxOutputTokens?: number;
}
@Controller('chat')
@ -32,6 +33,7 @@ export class AiController {
private readonly aiService: AiService,
private readonly featureFlagService: FeatureFlagService,
private readonly aiBillingService: AIBillingService,
private readonly aiModelRegistryService: AiModelRegistryService,
) {}
@Post()
@ -52,7 +54,7 @@ export class AiController {
);
}
const { messages, temperature, maxTokens } = request;
const { messages, temperature, maxOutputTokens } = request;
if (!messages || messages.length === 0) {
throw new HttpException(
@ -62,27 +64,26 @@ export class AiController {
}
try {
// TODO: Add support for custom models
const model = this.aiService.getModel(undefined);
const registeredModel = this.aiModelRegistryService.getDefaultModel();
const result = this.aiService.streamText({
messages,
options: {
temperature,
maxTokens,
model,
maxOutputTokens,
model: registeredModel.model,
},
});
result.usage.then((usage) => {
this.aiBillingService.calculateAndBillUsage(
model.modelId,
registeredModel.modelId,
usage,
workspace.id,
);
});
result.pipeDataStreamToResponse(res);
result.pipeUIMessageStreamToResponse(res);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';

View file

@ -11,8 +11,8 @@ describe('AIBillingService', () => {
let mockWorkspaceEventEmitter: jest.Mocked<WorkspaceEventEmitter>;
const mockTokenUsage = {
promptTokens: 1000,
completionTokens: 500,
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
};
@ -63,8 +63,8 @@ describe('AIBillingService', () => {
it('should calculate cost correctly with different token usage', async () => {
const differentTokenUsage = {
promptTokens: 2000,
completionTokens: 1000,
inputTokens: 2000,
outputTokens: 1000,
totalTokens: 3000,
};

View file

@ -1,17 +1,19 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { HttpException, HttpStatus } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { jsonSchema } from 'ai';
import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const';
import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
import { McpService } from 'src/engine/core-modules/ai/services/mcp.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const';
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { McpService } from 'src/engine/core-modules/ai/services/mcp.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
describe('McpService', () => {
let service: McpService;
@ -197,7 +199,7 @@ describe('McpService', () => {
const mockTool = {
description: 'Test tool',
parameters: { jsonSchema: { type: 'object', properties: {} } },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
execute: jest.fn().mockResolvedValue({ result: 'success' }),
};
@ -245,7 +247,7 @@ describe('McpService', () => {
const mockTool = {
description: 'Test tool',
parameters: { jsonSchema: { type: 'object', properties: {} } },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
execute: jest.fn().mockResolvedValue({ result: 'success' }),
};
@ -299,7 +301,7 @@ describe('McpService', () => {
const mockToolsMap = {
testTool: {
description: 'Test tool',
parameters: { jsonSchema: { type: 'object', properties: {} } },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
},
};
@ -328,7 +330,7 @@ describe('McpService', () => {
{
name: 'testTool',
description: 'Test tool',
inputSchema: { type: 'object', properties: {} },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
},
],
}),

View file

@ -1,5 +1,7 @@
import { Test } from '@nestjs/testing';
import { jsonSchema } from 'ai';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
@ -33,7 +35,7 @@ describe('ToolAdapterService', () => {
}));
const unflaggedTool: Tool = {
description: 'HTTP Request tool',
parameters: { type: 'object', properties: {} },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
execute: unflaggedToolExecute,
};
@ -44,7 +46,7 @@ describe('ToolAdapterService', () => {
}));
const flaggedTool: Tool = {
description: 'Send Email tool',
parameters: { type: 'object', properties: {} },
inputSchema: jsonSchema({ type: 'object', properties: {} }),
execute: flaggedToolExecute,
flag: PermissionFlagType.SEND_EMAIL_TOOL,
};

View file

@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { LanguageModelUsage } from 'ai';
import { type ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
@ -8,12 +10,6 @@ import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/bil
import { type BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
export interface TokenUsage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
@Injectable()
export class AIBillingService {
private readonly logger = new Logger(AIBillingService.name);
@ -23,7 +19,10 @@ export class AIBillingService {
private readonly aiModelRegistryService: AiModelRegistryService,
) {}
async calculateCost(modelId: ModelId, usage: TokenUsage): Promise<number> {
async calculateCost(
modelId: ModelId,
usage: LanguageModelUsage,
): Promise<number> {
const model = this.aiModelRegistryService.getEffectiveModelConfig(modelId);
if (!model) {
@ -31,9 +30,9 @@ export class AIBillingService {
}
const inputCost =
(usage.promptTokens / 1000) * model.inputCostPer1kTokensInCents;
((usage.inputTokens ?? 0) / 1000) * model.inputCostPer1kTokensInCents;
const outputCost =
(usage.completionTokens / 1000) * model.outputCostPer1kTokensInCents;
((usage.outputTokens ?? 0) / 1000) * model.outputCostPer1kTokensInCents;
const totalCost = inputCost + outputCost;
@ -46,7 +45,7 @@ export class AIBillingService {
async calculateAndBillUsage(
modelId: ModelId,
usage: TokenUsage,
usage: LanguageModelUsage,
workspaceId: string,
): Promise<void> {
const costInCents = await this.calculateCost(modelId, usage);

View file

@ -138,7 +138,7 @@ export class AiModelRegistryService {
return Array.from(this.modelRegistry.values());
}
getDefaultModel(): RegisteredAIModel | undefined {
getDefaultModel(): RegisteredAIModel {
const defaultModelId = this.twentyConfigService.get('DEFAULT_MODEL_ID');
let model = this.getModel(defaultModelId);

View file

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type CoreMessage, streamText, LanguageModelV1 } from 'ai';
import { LanguageModel, type ModelMessage, streamText } from 'ai';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
@ -28,18 +28,18 @@ export class AiService {
messages,
options,
}: {
messages: CoreMessage[];
messages: ModelMessage[];
options: {
temperature?: number;
maxTokens?: number;
model: LanguageModelV1;
maxOutputTokens?: number;
model: LanguageModel;
};
}) {
return streamText({
model: options.model,
messages,
temperature: options?.temperature,
maxTokens: options?.maxTokens,
maxOutputTokens: options?.maxOutputTokens,
});
}
}

View file

@ -204,11 +204,11 @@ export class McpService {
private handleToolsListing(id: string | number, toolSet: ToolSet) {
const toolsArray = Object.entries(toolSet)
.filter(([, def]) => !!def.parameters.jsonSchema)
.filter(([, def]) => !!def.inputSchema)
.map(([name, def]) => ({
name,
description: def.description,
inputSchema: def.parameters.jsonSchema,
inputSchema: def.inputSchema,
}));
return wrapJsonRpcResponse(id, {

View file

@ -42,7 +42,7 @@ export class ToolAdapterService {
private createToolSet(tool: Tool) {
return {
description: tool.description,
parameters: tool.parameters,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input),
};

View file

@ -61,7 +61,7 @@ export class ToolService {
if (objectPermission.canUpdate) {
tools[`create_${objectMetadata.nameSingular}`] = {
description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`,
parameters: getRecordInputSchema(objectMetadata),
inputSchema: getRecordInputSchema(objectMetadata),
execute: async (parameters) => {
return this.createRecord(
objectMetadata.nameSingular,
@ -74,7 +74,7 @@ export class ToolService {
tools[`update_${objectMetadata.nameSingular}`] = {
description: `Update an existing ${objectMetadata.labelSingular} record. Provide the record ID and only the fields you want to change. Unspecified fields will remain unchanged. Returns the updated record with all current data.`,
parameters: getRecordInputSchema(objectMetadata),
inputSchema: getRecordInputSchema(objectMetadata),
execute: async (parameters) => {
return this.updateRecord(
objectMetadata.nameSingular,
@ -89,7 +89,7 @@ export class ToolService {
if (objectPermission.canRead) {
tools[`find_${objectMetadata.nameSingular}`] = {
description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination. Returns an array of matching records with their full data.`,
parameters: generateFindToolSchema(objectMetadata),
inputSchema: generateFindToolSchema(objectMetadata),
execute: async (parameters) => {
return this.findRecords(
objectMetadata.nameSingular,
@ -102,7 +102,7 @@ export class ToolService {
tools[`find_one_${objectMetadata.nameSingular}`] = {
description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`,
parameters: generateFindOneToolSchema(),
inputSchema: generateFindOneToolSchema(),
execute: async (parameters) => {
return this.findOneRecord(
objectMetadata.nameSingular,
@ -117,7 +117,7 @@ export class ToolService {
if (objectPermission.canSoftDelete) {
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
parameters: generateSoftDeleteToolSchema(),
inputSchema: generateSoftDeleteToolSchema(),
execute: async (parameters) => {
return this.softDeleteRecord(
objectMetadata.nameSingular,
@ -130,7 +130,7 @@ export class ToolService {
tools[`soft_delete_many_${objectMetadata.nameSingular}`] = {
description: `Soft delete multiple ${objectMetadata.labelSingular} records at once by providing an array of record IDs. All records are marked as deleted but remain in the database. This is efficient for bulk operations and preserves all data.`,
parameters: generateBulkDeleteToolSchema(),
inputSchema: generateBulkDeleteToolSchema(),
execute: async (parameters) => {
return this.softDeleteManyRecords(
objectMetadata.nameSingular,

View file

@ -3,6 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { AuditContextMock } from 'test/utils/audit-context.mock';
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@ -135,7 +136,7 @@ describe('AuditService', () => {
const context = service.createContext(mockUserIdAndWorkspaceId);
const result = await context.createObjectEvent(
CUSTOM_DOMAIN_ACTIVATED_EVENT,
OBJECT_RECORD_CREATED_EVENT,
{
recordId: 'test-record-id',
objectMetadataId: 'test-object-metadata-id',

View file

@ -1,10 +1,8 @@
import { z } from 'zod';
export const baseEventSchema = z
.object({
timestamp: z.string(),
userId: z.string().nullish(),
workspaceId: z.string().nullish(),
version: z.string(),
})
.strict();
export const baseEventSchema = z.strictObject({
timestamp: z.string(),
userId: z.string().nullish(),
workspaceId: z.string().nullish(),
version: z.string(),
});

View file

@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
export const objectRecordCreatedSchema = z.object({
event: z.literal(OBJECT_RECORD_CREATED_EVENT),
properties: z.object({}).passthrough(),
properties: z.looseObject({}),
});
export type ObjectRecordCreatedTrackEvent = z.infer<

View file

@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
export const objectRecordDeletedSchema = z.object({
event: z.literal(OBJECT_RECORD_DELETED_EVENT),
properties: z.object({}).passthrough(),
properties: z.looseObject({}),
});
export type ObjectRecordDeletedTrackEvent = z.infer<

View file

@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
export const objectRecordUpdatedSchema = z.object({
event: z.literal(OBJECT_RECORD_UPDATED_EVENT),
properties: z.object({}).passthrough(),
properties: z.looseObject({}),
});
export type ObjectRecordUpdatedTrackEvent = z.infer<

View file

@ -3,12 +3,10 @@ import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
export const customDomainActivatedSchema = z
.object({
event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT),
properties: z.object({}).strict(),
})
.strict();
export const customDomainActivatedSchema = z.strictObject({
event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT),
properties: z.strictObject({}),
});
export type CustomDomainActivatedTrackEvent = z.infer<
typeof customDomainActivatedSchema

View file

@ -4,12 +4,10 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
'Custom Domain Deactivated' as const;
export const customDomainDeactivatedSchema = z
.object({
event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT),
properties: z.object({}).strict(),
})
.strict();
export const customDomainDeactivatedSchema = z.strictObject({
event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT),
properties: z.strictObject({}),
});
export type CustomDomainDeactivatedTrackEvent = z.infer<
typeof customDomainDeactivatedSchema

View file

@ -3,19 +3,15 @@ import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const MONITORING_EVENT = 'Monitoring' as const;
export const monitoringSchema = z
.object({
event: z.literal(MONITORING_EVENT),
properties: z
.object({
eventName: z.string(),
connectedAccountId: z.string().optional(),
messageChannelId: z.string().optional(),
message: z.string().optional(),
})
.strict(),
})
.strict();
export const monitoringSchema = z.strictObject({
event: z.literal(MONITORING_EVENT),
properties: z.strictObject({
eventName: z.string(),
connectedAccountId: z.string().optional(),
messageChannelId: z.string().optional(),
message: z.string().optional(),
}),
});
export type MonitoringTrackEvent = z.infer<typeof monitoringSchema>;

View file

@ -4,20 +4,16 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
'Serverless Function Executed' as const;
export const serverlessFunctionExecutedSchema = z
.object({
event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT),
properties: z
.object({
duration: z.number(),
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
errorType: z.string().optional(),
functionId: z.string(),
functionName: z.string(),
})
.strict(),
})
.strict();
export const serverlessFunctionExecutedSchema = z.strictObject({
event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT),
properties: z.strictObject({
duration: z.number(),
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
errorType: z.string().optional(),
functionId: z.string(),
functionName: z.string(),
}),
});
export type ServerlessFunctionExecutedTrackEvent = z.infer<
typeof serverlessFunctionExecutedSchema

View file

@ -3,12 +3,10 @@ import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const USER_SIGNUP_EVENT = 'User Signup' as const;
export const userSignupSchema = z
.object({
event: z.literal(USER_SIGNUP_EVENT),
properties: z.object({}).strict(),
})
.strict();
export const userSignupSchema = z.strictObject({
event: z.literal(USER_SIGNUP_EVENT),
properties: z.strictObject({}),
});
export type UserSignupTrackEvent = z.infer<typeof userSignupSchema>;

View file

@ -3,20 +3,16 @@ import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
export const webhookResponseSchema = z
.object({
event: z.literal(WEBHOOK_RESPONSE_EVENT),
properties: z
.object({
status: z.number().optional(),
success: z.boolean(),
url: z.string(),
webhookId: z.string(),
eventName: z.string(),
})
.strict(),
})
.strict();
export const webhookResponseSchema = z.strictObject({
event: z.literal(WEBHOOK_RESPONSE_EVENT),
properties: z.strictObject({
status: z.number().optional(),
success: z.boolean(),
url: z.string(),
webhookId: z.string(),
eventName: z.string(),
}),
});
export type WebhookResponseTrackEvent = z.infer<typeof webhookResponseSchema>;

View file

@ -4,14 +4,12 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
export const WORKSPACE_ENTITY_CREATED_EVENT =
'Workspace Entity Created' as const;
export const workspaceEntityCreatedSchema = z
.object({
event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT),
properties: z.object({
name: z.string(),
}),
})
.strict();
export const workspaceEntityCreatedSchema = z.strictObject({
event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT),
properties: z.strictObject({
name: z.string(),
}),
});
export type WorkspaceEntityCreatedTrackEvent = z.infer<
typeof workspaceEntityCreatedSchema

View file

@ -9,7 +9,7 @@ import { type ConnectionParameters } from 'src/engine/core-modules/imap-smtp-cal
export class ImapSmtpCaldavValidatorService {
private readonly protocolConnectionSchema = z.object({
host: z.string().min(1, 'Host is required'),
port: z.number().int().positive('Port must be a positive number'),
port: z.int().positive('Port must be a positive number'),
username: z.string().optional(),
password: z.string().min(1, 'Password is required'),
secure: z.boolean().optional(),
@ -29,7 +29,7 @@ export class ImapSmtpCaldavValidatorService {
return this.protocolConnectionSchema.parse(params);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors
const errorMessages = error.issues
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');

View file

@ -18,7 +18,7 @@ export class ToolRegistryService {
ToolType.SEND_EMAIL,
() => ({
description: this.sendEmailTool.description,
parameters: this.sendEmailTool.parameters,
inputSchema: this.sendEmailTool.inputSchema,
execute: (params) =>
this.sendEmailTool.execute(params as SendEmailInput),
flag: PermissionFlagType.SEND_EMAIL_TOOL,

View file

@ -6,7 +6,7 @@ export const HttpRequestInputZodSchema = z.object({
.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
.describe('The HTTP method to use'),
headers: z
.record(z.string())
.record(z.string(), z.string())
.optional()
.describe('HTTP headers to include in the request'),
body: z

View file

@ -13,7 +13,7 @@ import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
export class HttpTool implements Tool {
description =
'Make an HTTP request to any URL with configurable method, headers, and body.';
parameters = HttpToolParametersZodSchema;
inputSchema = HttpToolParametersZodSchema;
async execute(parameters: ToolInput): Promise<ToolOutput> {
const { url, method, headers, body } = parameters as HttpRequestInput;

View file

@ -1,11 +1,10 @@
import { z } from 'zod';
export const SendEmailInputZodSchema = z.object({
email: z.string().email().describe('The recipient email address'),
email: z.email().describe('The recipient email address'),
subject: z.string().describe('The email subject line'),
body: z.string().describe('The email body content (HTML or plain text)'),
connectedAccountId: z
.string()
.uuid()
.describe(
'The UUID of the connected account to send the email from. Provide this only if you have it; otherwise, leave blank.',

View file

@ -26,7 +26,7 @@ export class SendEmailTool implements Tool {
description =
'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.';
parameters = SendEmailToolParametersZodSchema;
inputSchema = SendEmailToolParametersZodSchema;
constructor(
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
@ -91,7 +91,10 @@ export class SendEmailTool implements Tool {
let { connectedAccountId } = parameters;
try {
const emailSchema = z.string().trim().email('Invalid email');
const emailSchema = z
.string()
.trim()
.pipe(z.email({ error: 'Invalid email' }));
const emailValidation = emailSchema.safeParse(email);
if (!emailValidation.success) {

View file

@ -1,5 +1,4 @@
import { type JSONSchema7 } from 'json-schema';
import { type ZodType } from 'zod';
import { type FlexibleSchema } from '@ai-sdk/provider-utils';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
@ -7,7 +6,7 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions
export type Tool = {
description: string;
parameters: JSONSchema7 | ZodType;
inputSchema: FlexibleSchema<unknown>;
execute(input: ToolInput): Promise<ToolOutput>;
flag?: PermissionFlagType;
};

View file

@ -33,33 +33,28 @@ export type TOTPStrategyConfig = z.infer<typeof TOTP_STRATEGY_CONFIG_SCHEMA>;
export const TOTP_STRATEGY_CONFIG_SCHEMA = z.object({
algorithm: z
.nativeEnum(TOTPHashAlgorithms, {
errorMap: () => ({
message:
'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.',
}),
.enum(TOTPHashAlgorithms, {
error: () =>
'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.',
})
.optional(),
digits: z
.number({
invalid_type_error: 'Digits must be a number.',
.int({
error: 'Digits must be a whole number.',
})
.min(6, {
error: 'Digits must be at least 6.',
})
.max(8, {
error: 'Digits cannot be more than 8.',
})
.int({ message: 'Digits must be a whole number.' })
.min(6, { message: 'Digits must be at least 6.' })
.max(8, { message: 'Digits cannot be more than 8.' })
.optional(),
encodings: z
.nativeEnum(TOTPKeyEncodings, {
errorMap: () => ({ message: 'Invalid encoding specified.' }),
.enum(TOTPKeyEncodings, {
error: () => 'Invalid encoding specified.',
})
.optional(),
window: z.number().int().min(0).optional(),
step: z
.number({
invalid_type_error: 'Step must be a number.',
})
.int()
.min(1)
.optional(),
epoch: z.number().int().min(0).optional(),
window: z.int().min(0).optional(),
step: z.int().min(1).optional(),
epoch: z.int().min(0).optional(),
});

View file

@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { authenticator } from 'otplib';
import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type SafeParseReturnType } from 'zod';
import { type ZodSafeParseResult } from 'zod';
import { type OTPAuthenticationStrategyInterface } from 'src/engine/core-modules/two-factor-authentication/strategies/otp/interfaces/otp.strategy.interface';
@ -24,7 +24,7 @@ export class TotpStrategy implements OTPAuthenticationStrategyInterface {
public readonly name = TwoFactorAuthenticationStrategy.TOTP;
constructor(options?: TOTPStrategyConfig) {
let result: SafeParseReturnType<unknown, TOTPStrategyConfig> | undefined;
let result: ZodSafeParseResult<TOTPStrategyConfig> | undefined;
if (isDefined(options)) {
result = TOTP_STRATEGY_CONFIG_SCHEMA.safeParse(options);

View file

@ -2,13 +2,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
type CoreMessage,
type CoreUserMessage,
type FilePart,
type ImagePart,
LanguageModelUsage,
type ModelMessage,
streamText,
ToolSet,
type UserContent,
UserModelMessage,
} from 'ai';
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
@ -40,11 +41,7 @@ import { AgentException, AgentExceptionCode } from './agent.exception';
export interface AgentExecutionResult {
result: object;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
usage: LanguageModelUsage;
}
@Injectable()
@ -68,14 +65,12 @@ export class AgentExecutionService {
async prepareAIRequestConfig({
messages,
prompt,
system,
agent,
}: {
system: string;
agent: AgentEntity | null;
prompt?: string;
messages?: CoreMessage[];
messages: ModelMessage[];
}) {
try {
if (agent) {
@ -111,8 +106,7 @@ export class AgentExecutionService {
system,
tools,
model: registeredModel.model,
...(messages && { messages }),
...(prompt && { prompt }),
messages,
maxSteps: AGENT_CONFIG.MAX_STEPS,
...(registeredModel.doesSupportThinking && {
providerOptions: {
@ -149,7 +143,7 @@ export class AgentExecutionService {
private async buildUserMessage(
userMessage: string,
fileIds: string[],
): Promise<CoreUserMessage> {
): Promise<UserModelMessage> {
const content: Exclude<UserContent, string> = [
{
type: 'text',
@ -248,22 +242,22 @@ export class AgentExecutionService {
return {
type: 'image',
image: fileBuffer,
mimeType: file.type,
mediaType: file.type,
};
}
return {
type: 'file',
data: fileBuffer,
mimeType: file.type,
mediaType: file.type,
};
}
private mapMessagesToCoreMessages(
messages: AgentChatMessageEntity[],
): CoreMessage[] {
): ModelMessage[] {
return messages
.map(({ role, rawContent }): CoreMessage => {
.map(({ role, rawContent }): ModelMessage => {
if (role === AgentChatMessageRole.USER) {
return {
role: 'user',
@ -300,7 +294,8 @@ export class AgentExecutionService {
where: { id: agentId },
});
const llmMessages: CoreMessage[] = this.mapMessagesToCoreMessages(messages);
const llmMessages: ModelMessage[] =
this.mapMessagesToCoreMessages(messages);
let contextString = '';

View file

@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CoreMessage, generateText } from 'ai';
import { ModelMessage, generateText } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
@ -15,7 +15,7 @@ export type HandoffRequest = {
fromAgentId: string;
toAgentId: string;
workspaceId: string;
messages?: CoreMessage[];
messages: ModelMessage[];
};
@Injectable()

View file

@ -35,7 +35,7 @@ export class AgentHandoffToolService {
'{agentName}',
handoff.toAgent.name,
),
parameters: AGENT_HANDOFF_SCHEMA,
inputSchema: AGENT_HANDOFF_SCHEMA,
execute: async ({ input }) => {
const result = await this.agentHandoffExecutorService.executeHandoff({
fromAgentId: agentId,

View file

@ -25,6 +25,16 @@ export type StreamAgentChatOptions = {
res: Response;
};
const CLIENT_FORWARDED_EVENT_TYPES = [
'text-delta',
'reasoning',
'reasoning-delta',
'tool-call',
'tool-input-delta',
'tool-result',
'error',
];
@Injectable()
export class AgentStreamingService {
private readonly logger = new Logger(AgentStreamingService.name);
@ -81,14 +91,7 @@ export class AgentStreamingService {
this.sendStreamEvent(
res,
[
'text-delta',
'reasoning',
'reasoning-signature',
'tool-call',
'tool-result',
'error',
].includes(chunk.type)
CLIENT_FORWARDED_EVENT_TYPES.includes(chunk.type)
? chunk
: { type: chunk.type },
);

View file

@ -26,24 +26,16 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
}),
z.object({
type: z.literal('image'),
image: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
image: z
.string()
.describe('Base64 encoded image data or URL'),
mediaType: z.string().optional(),
}),
z.object({
type: z.literal('file'),
data: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
data: z
.string()
.describe('Base64 encoded file data or URL'),
mediaType: z.string(),
}),
]),
@ -62,13 +54,9 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
}),
z.object({
type: z.literal('file'),
data: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
data: z
.string()
.describe('Base64 encoded file data or URL'),
mediaType: z.string(),
filename: z.string().optional(),
}),
@ -80,7 +68,7 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
type: z.literal('tool-call'),
toolCallId: z.string(),
toolName: z.string(),
input: z.record(z.any()),
input: z.record(z.string(), z.any()),
}),
]),
),

View file

@ -1,60 +1,96 @@
import { type ReasoningPart } from '@ai-sdk/provider-utils';
import { type TextPart } from 'ai';
type ReasoningPart = {
type: 'reasoning';
text: string;
signature: string;
};
import {
parseStreamLine,
splitStreamIntoLines,
type TextBlock,
} from 'twenty-shared/ai';
export const constructAssistantMessageContentFromStream = (
rawContent: string,
) => {
const lines = rawContent.trim().split('\n');
const lines = splitStreamIntoLines(rawContent);
const output: Array<TextPart | ReasoningPart> = [];
let reasoningText = '';
let textContent = '';
let currentTextBlock: TextBlock = null;
const flushTextBlock = () => {
if (currentTextBlock) {
if (currentTextBlock.type === 'reasoning') {
output.push({
type: 'reasoning',
text: currentTextBlock.content,
});
} else {
output.push({
type: 'text',
text: currentTextBlock.content,
});
}
currentTextBlock = null;
}
};
for (const line of lines) {
let event;
const event = parseStreamLine(line);
try {
event = JSON.parse(line);
} catch {
if (!event) {
continue;
}
switch (event.type) {
case 'reasoning':
reasoningText += event.textDelta || '';
case 'reasoning-start':
flushTextBlock();
currentTextBlock = {
type: 'reasoning',
content: '',
isThinking: true,
};
break;
case 'reasoning-signature':
if (reasoningText) {
output.push({
case 'reasoning-delta':
if (!currentTextBlock || currentTextBlock.type !== 'reasoning') {
flushTextBlock();
currentTextBlock = {
type: 'reasoning',
text: reasoningText,
signature: event.signature,
});
reasoningText = '';
content: '',
isThinking: true,
};
}
currentTextBlock.content += event.text || '';
break;
case 'reasoning-end':
if (currentTextBlock?.type === 'reasoning') {
currentTextBlock.isThinking = false;
}
break;
case 'text-delta':
textContent += event.textDelta || '';
if (!currentTextBlock || currentTextBlock.type !== 'text') {
flushTextBlock();
currentTextBlock = { type: 'text', content: '' };
}
currentTextBlock.content += event.text || '';
break;
case 'step-finish':
if (currentTextBlock?.type === 'reasoning') {
currentTextBlock.isThinking = false;
}
flushTextBlock();
break;
case 'error':
flushTextBlock();
break;
default:
if (textContent) {
output.push({
type: 'text',
text: textContent,
});
textContent = '';
}
break;
}
}
flushTextBlock();
return output;
};

View file

@ -55,7 +55,7 @@ export class BlocklistValidationService {
const emailOrDomainSchema = z
.string()
.trim()
.email('Invalid email or domain')
.pipe(z.email({ error: 'Invalid email or domain' }))
.or(
z
.string()
@ -73,7 +73,7 @@ export class BlocklistValidationService {
const result = emailOrDomainSchema.safeParse(handle);
if (!result.success) {
throw new BadRequestException(result.error.errors[0].message);
throw new BadRequestException(result.error.issues[0].message);
}
}
}

View file

@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { generateObject, generateText, ToolSet } from 'ai';
import { generateObject, generateText, stepCountIs, ToolSet } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
@ -98,7 +98,7 @@ export class AiAgentExecutorService {
tools,
model: registeredModel.model,
prompt: userPrompt,
maxSteps: AGENT_CONFIG.MAX_STEPS,
stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS),
});
if (Object.keys(schema).length === 0) {
@ -121,12 +121,12 @@ export class AiAgentExecutorService {
return {
result: output.object,
usage: {
promptTokens:
(textResponse.usage?.promptTokens ?? 0) +
(output.usage?.promptTokens ?? 0),
completionTokens:
(textResponse.usage?.completionTokens ?? 0) +
(output.usage?.completionTokens ?? 0),
inputTokens:
(textResponse.usage?.inputTokens ?? 0) +
(output.usage?.inputTokens ?? 0),
outputTokens:
(textResponse.usage?.outputTokens ?? 0) +
(output.usage?.outputTokens ?? 0),
totalTokens:
(textResponse.usage?.totalTokens ?? 0) +
(output.usage?.totalTokens ?? 0),

View file

@ -1,11 +1,9 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { z } from 'zod';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { workflowActionSchema, workflowTriggerSchema } from './workflow.schema';
export const createWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()

View file

@ -1,27 +1,28 @@
import {
FieldMetadataType,
StepLogicalOperator,
ViewFilterOperand,
} from 'twenty-shared/types';
import { StepStatus } from 'twenty-shared/workflow';
import { z } from 'zod';
import { FieldMetadataType } from '../../types/FieldMetadataType';
import { StepLogicalOperator } from '../../types/StepFilters';
import { ViewFilterOperand } from '../../types/ViewFilterOperand';
import { StepStatus } from '../types/WorkflowRunStateStepInfos';
// Base schemas
export const objectRecordSchema = z
.record(z.any())
.record(z.string(), z.any())
.describe(
'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' +
'- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' +
'- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' +
'- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}',
'- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' +
'- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' +
'- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}',
);
export const baseWorkflowActionSettingsSchema = z.object({
input: z
.object({})
.passthrough()
.describe('Input data for the workflow action. Structure depends on the action type.'),
.looseObject({})
.describe(
'Input data for the workflow action. Structure depends on the action type.',
),
outputSchema: z
.object({})
.passthrough()
.looseObject({})
.describe(
'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.',
),
@ -30,7 +31,9 @@ export const baseWorkflowActionSettingsSchema = z.object({
value: z.boolean().describe('Whether to retry the action if it fails.'),
}),
continueOnFailure: z.object({
value: z.boolean().describe('Whether to continue to the next step if this action fails.'),
value: z
.boolean()
.describe('Whether to continue to the next step if this action fails.'),
}),
}),
});
@ -38,18 +41,26 @@ export const baseWorkflowActionSettingsSchema = z.object({
export const baseWorkflowActionSchema = z.object({
id: z
.string()
.describe('Unique identifier for the workflow step. Must be unique within the workflow.'),
.describe(
'Unique identifier for the workflow step. Must be unique within the workflow.',
),
name: z
.string()
.describe('Human-readable name for the workflow step. Should clearly describe what the step does.'),
.describe(
'Human-readable name for the workflow step. Should clearly describe what the step does.',
),
valid: z
.boolean()
.describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'),
.describe(
'Whether the step configuration is valid. Set to true when all required fields are properly configured.',
),
nextStepIds: z
.array(z.string())
.optional()
.nullable()
.describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'),
.describe(
'Array of step IDs that this step connects to. Leave empty or null for the final step.',
),
position: z
.object({ x: z.number(), y: z.number() })
.optional()
@ -61,7 +72,9 @@ export const baseTriggerSchema = z.object({
name: z
.string()
.optional()
.describe('Human-readable name for the trigger. Optional but recommended for clarity.'),
.describe(
'Human-readable name for the trigger. Optional but recommended for clarity.',
),
type: z
.enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK'])
.describe(
@ -71,21 +84,24 @@ export const baseTriggerSchema = z.object({
.object({ x: z.number(), y: z.number() })
.optional()
.nullable()
.describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'),
.describe(
'Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.',
),
nextStepIds: z
.array(z.string())
.optional()
.nullable()
.describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'),
.describe(
'Array of step IDs that the trigger connects to. These are the first steps in the workflow.',
),
});
// Action settings schemas
export const workflowCodeActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
serverlessFunctionId: z.string(),
serverlessFunctionVersion: z.string(),
serverlessFunctionInput: z.record(z.any()),
serverlessFunctionInput: z.record(z.string(), z.any()),
}),
});
@ -107,10 +123,7 @@ export const workflowCreateRecordActionSettingsSchema =
.describe(
'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").',
),
objectRecord: objectRecordSchema
.describe(
'The record data to create.',
)
objectRecord: objectRecordSchema.describe('The record data to create.'),
}),
});
@ -162,7 +175,7 @@ export const workflowFormActionSettingsSchema =
z.literal('RECORD'),
]),
placeholder: z.string().optional(),
settings: z.record(z.any()).optional(),
settings: z.record(z.string(), z.any()).optional(),
value: z.any().optional(),
}),
),
@ -173,9 +186,10 @@ export const workflowHttpRequestActionSettingsSchema =
input: z.object({
url: z.string(),
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
headers: z.record(z.string()).optional(),
headers: z.record(z.string(), z.string()).optional(),
body: z
.record(
z.string(),
z.union([
z.string(),
z.number(),
@ -200,30 +214,48 @@ export const workflowAiAgentActionSettingsSchema =
export const workflowFilterActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
stepFilterGroups: z.array(z.object({
id: z.string(),
logicalOperator: z.nativeEnum(StepLogicalOperator),
parentStepFilterGroupId: z.string().optional(),
positionInStepFilterGroup: z.number().optional(),
})),
stepFilters: z.array(z.object({
id: z.string(),
type: z.string(),
stepOutputKey: z.string(),
operand: z.nativeEnum(ViewFilterOperand),
value: z.string(),
stepFilterGroupId: z.string(),
positionInStepFilterGroup: z.number().optional(),
fieldMetadataId: z.string().optional(),
compositeFieldSubFieldName: z.string().optional(),
})),
stepFilterGroups: z.array(
z.object({
id: z.string(),
logicalOperator: z.enum(StepLogicalOperator),
parentStepFilterGroupId: z.string().optional(),
positionInStepFilterGroup: z.number().optional(),
}),
),
stepFilters: z.array(
z.object({
id: z.string(),
type: z.string(),
stepOutputKey: z.string(),
operand: z.enum(ViewFilterOperand),
value: z.string(),
stepFilterGroupId: z.string(),
positionInStepFilterGroup: z.number().optional(),
fieldMetadataId: z.string().optional(),
compositeFieldSubFieldName: z.string().optional(),
}),
),
}),
});
export const workflowIteratorActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
items: z.union([z.array(z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.any()), z.any()])), z.string()]).optional(),
items: z
.union([
z.array(
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.record(z.string(), z.any()),
z.any(),
]),
),
z.string(),
])
.optional(),
initialLoopStepIds: z.array(z.string()).optional(),
}),
});
@ -233,7 +265,6 @@ export const workflowEmptyActionSettingsSchema =
input: z.object({}),
});
// Action schemas
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
settings: workflowCodeActionSettingsSchema,
@ -244,20 +275,26 @@ export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
settings: workflowSendEmailActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
},
);
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
});
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
},
);
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
});
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
},
);
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FIND_RECORDS'),
@ -294,7 +331,6 @@ export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({
settings: workflowEmptyActionSettingsSchema,
});
// Combined action schema
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
workflowSendEmailActionSchema,
@ -310,49 +346,50 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
workflowEmptyActionSchema,
]);
// Trigger schemas
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z
.string()
.regex(
/^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/,
'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")',
)
.describe(
'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.',
),
input: z.object({}).passthrough().optional(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.',
),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
}),
}).describe(
'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.',
);
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema
.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z
.string()
.regex(
/^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/,
'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")',
)
.describe(
'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.',
),
input: z.looseObject({}).optional(),
outputSchema: z
.looseObject({})
.describe(
'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.',
),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
}),
})
.describe(
'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.',
);
export const workflowManualTriggerSchema = baseTriggerSchema.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
}).describe(
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
);
export const workflowManualTriggerSchema = baseTriggerSchema
.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z
.looseObject({})
.describe(
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
})
.describe(
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
);
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
type: z.literal('CRON'),
@ -364,7 +401,7 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({
hour: z.number().min(0).max(23),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
}),
z.object({
type: z.literal('HOURS'),
@ -372,17 +409,17 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({
hour: z.number().min(1),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
}),
z.object({
type: z.literal('MINUTES'),
schedule: z.object({ minute: z.number().min(1) }),
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
}),
z.object({
type: z.literal('CUSTOM'),
pattern: z.string(),
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
}),
]),
});
@ -391,20 +428,19 @@ export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({
type: z.literal('WEBHOOK'),
settings: z.discriminatedUnion('httpMethod', [
z.object({
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
httpMethod: z.literal('GET'),
authentication: z.literal('API_KEY').nullable(),
}),
z.object({
outputSchema: z.object({}).passthrough(),
outputSchema: z.looseObject({}),
httpMethod: z.literal('POST'),
expectedBody: z.object({}).passthrough(),
expectedBody: z.looseObject({}),
authentication: z.literal('API_KEY').nullable(),
}),
]),
});
// Combined trigger schema
export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
@ -412,7 +448,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowWebhookTriggerSchema,
]);
export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus);
export const workflowRunStepStatusSchema = z.enum(StepStatus);
export const workflowRunStateStepInfoSchema = z.object({
result: z.any().optional(),
@ -421,6 +457,7 @@ export const workflowRunStateStepInfoSchema = z.object({
});
export const workflowRunStateStepInfosSchema = z.record(
z.string(),
workflowRunStateStepInfoSchema,
);
@ -441,17 +478,15 @@ export const workflowRunStatusSchema = z.enum([
'ENQUEUED',
]);
export const workflowRunSchema = z
.object({
__typename: z.literal('WorkflowRun'),
id: z.string(),
workflowVersionId: z.string(),
workflowId: z.string(),
state: workflowRunStateSchema.nullable(),
status: workflowRunStatusSchema,
createdAt: z.string(),
deletedAt: z.string().nullable(),
endedAt: z.string().nullable(),
name: z.string(),
})
.passthrough();
export const workflowRunSchema = z.looseObject({
__typename: z.literal('WorkflowRun'),
id: z.string(),
workflowVersionId: z.string(),
workflowId: z.string(),
state: workflowRunStateSchema.nullable(),
status: workflowRunStatusSchema,
createdAt: z.string(),
deletedAt: z.string().nullable(),
endedAt: z.string().nullable(),
name: z.string(),
});

View file

@ -71,7 +71,7 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples,
- Error handling options
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
parameters: createCompleteWorkflowSchema,
inputSchema: createCompleteWorkflowSchema,
execute: async (parameters: {
name: string;
description?: string;
@ -160,7 +160,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.create_workflow_version_step = {
description:
'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.',
parameters: createWorkflowVersionStepSchema,
inputSchema: createWorkflowVersionStepSchema,
execute: async (parameters: CreateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.createWorkflowVersionStep(
@ -182,7 +182,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.update_workflow_version_step = {
description:
'Update an existing step in a workflow version. This modifies the step configuration.',
parameters: updateWorkflowVersionStepSchema,
inputSchema: updateWorkflowVersionStepSchema,
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.updateWorkflowVersionStep(
@ -205,7 +205,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.delete_workflow_version_step = {
description:
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
parameters: deleteWorkflowVersionStepSchema,
inputSchema: deleteWorkflowVersionStepSchema,
execute: async (parameters: {
workflowVersionId: string;
stepId: string;
@ -231,7 +231,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.create_workflow_version_edge = {
description:
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
parameters: createWorkflowVersionEdgeSchema,
inputSchema: createWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
@ -258,7 +258,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.delete_workflow_version_edge = {
description: 'Delete a connection (edge) between workflow steps.',
parameters: deleteWorkflowVersionEdgeSchema,
inputSchema: deleteWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
@ -286,7 +286,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.create_draft_from_workflow_version = {
description:
'Create a new draft workflow version from an existing one. This allows for iterative workflow development.',
parameters: createDraftFromWorkflowVersionSchema,
inputSchema: createDraftFromWorkflowVersionSchema,
execute: async (parameters: {
workflowId: string;
workflowVersionIdToCopy: string;
@ -312,7 +312,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.update_workflow_version_positions = {
description:
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
parameters: updateWorkflowVersionPositionsSchema,
inputSchema: updateWorkflowVersionPositionsSchema,
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
try {
return await this.workflowVersionService.updateWorkflowVersionPositions(
@ -335,7 +335,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.activate_workflow_version = {
description:
'Activate a workflow version. This makes the workflow version active and available for execution.',
parameters: activateWorkflowVersionSchema,
inputSchema: activateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.activateWorkflowVersion(
@ -354,7 +354,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.deactivate_workflow_version = {
description:
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
parameters: deactivateWorkflowVersionSchema,
inputSchema: deactivateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.deactivateWorkflowVersion(
@ -373,7 +373,7 @@ This is the most efficient way for AI to create workflows as it handles all the
tools.compute_step_output_schema = {
description:
'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.',
parameters: computeStepOutputSchemaSchema,
inputSchema: computeStepOutputSchemaSchema,
execute: async (parameters: {
step: WorkflowTrigger | WorkflowAction;
}) => {

View file

@ -109,7 +109,7 @@ export const createAgentToolTestModule =
provide: SendEmailTool,
useValue: {
description: 'mock',
parameters: {},
inputSchema: {},
execute: jest.fn(),
},
},

View file

@ -38,6 +38,11 @@
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./ai": {
"types": "./dist/ai/index.d.ts",
"import": "./dist/ai.mjs",
"require": "./dist/ai.cjs"
},
"./constants": {
"types": "./dist/constants/index.d.ts",
"import": "./dist/constants.mjs",
@ -76,6 +81,7 @@
},
"files": [
"dist",
"ai",
"constants",
"testing",
"translations",
@ -86,6 +92,9 @@
],
"typesVersions": {
"*": {
"ai": [
"dist/ai/index.d.ts"
],
"constants": [
"dist/constants/index.d.ts"
],

View file

@ -14,6 +14,8 @@
],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/ai/package.json",
"{projectRoot}/ai/dist",
"{projectRoot}/constants/package.json",
"{projectRoot}/constants/dist",
"{projectRoot}/testing/package.json",

View file

@ -0,0 +1,19 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export type { ErrorEvent } from './types/ErrorEvent';
export type { ReasoningDeltaEvent } from './types/ReasoningDeltaEvent';
export type { StreamEvent } from './types/StreamEvent';
export type { TextBlock } from './types/TextBlock';
export type { TextDeltaEvent } from './types/TextDeltaEvent';
export type { ToolCallEvent } from './types/ToolCallEvent';
export type { ToolEvent } from './types/ToolEvent';
export type { ToolResultEvent } from './types/ToolResultEvent';
export { parseStreamLine } from './utils/parseStreamLine';
export { splitStreamIntoLines } from './utils/splitStreamIntoLines';

View file

@ -0,0 +1,5 @@
export type ErrorEvent = {
type: 'error';
message: string;
error?: unknown;
};

View file

@ -0,0 +1,4 @@
export type ReasoningDeltaEvent = {
type: 'reasoning-delta';
text: string;
};

View file

@ -0,0 +1,21 @@
import type { ErrorEvent } from './ErrorEvent';
import type { ReasoningDeltaEvent } from './ReasoningDeltaEvent';
import type { TextDeltaEvent } from './TextDeltaEvent';
import type { ToolCallEvent } from './ToolCallEvent';
import type { ToolResultEvent } from './ToolResultEvent';
export type StreamEvent =
| ToolCallEvent
| ToolResultEvent
| {
type: 'reasoning-start';
}
| ReasoningDeltaEvent
| {
type: 'reasoning-end';
}
| TextDeltaEvent
| {
type: 'step-finish';
}
| ErrorEvent;

View file

@ -0,0 +1,4 @@
export type TextBlock =
| { type: 'reasoning'; content: string; isThinking: boolean }
| { type: 'text'; content: string }
| null;

View file

@ -0,0 +1,4 @@
export type TextDeltaEvent = {
type: 'text-delta';
text: string;
};

View file

@ -0,0 +1,9 @@
export type ToolCallEvent = {
type: 'tool-call';
toolCallId: string;
toolName: string;
input: {
loadingMessage: string;
input: unknown;
};
};

View file

@ -0,0 +1,4 @@
import type { ToolCallEvent } from './ToolCallEvent';
import type { ToolResultEvent } from './ToolResultEvent';
export type ToolEvent = ToolCallEvent | ToolResultEvent;

View file

@ -0,0 +1,11 @@
export type ToolResultEvent = {
type: 'tool-result';
toolCallId: string;
toolName: string;
output: {
success: boolean;
result?: unknown;
error?: string;
message: string;
};
};

View file

@ -0,0 +1,9 @@
import type { StreamEvent } from '../types/StreamEvent';
export const parseStreamLine = (line: string): StreamEvent | null => {
try {
return JSON.parse(line) as StreamEvent;
} catch {
return null;
}
};

View file

@ -0,0 +1,3 @@
export const splitStreamIntoLines = (streamText: string): string[] => {
return streamText.trim().split('\n');
};

View file

@ -9,48 +9,6 @@
export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/contentTypeValuesHttpRequest';
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
export {
objectRecordSchema,
baseWorkflowActionSettingsSchema,
baseWorkflowActionSchema,
baseTriggerSchema,
workflowCodeActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
workflowCreateRecordActionSettingsSchema,
workflowUpdateRecordActionSettingsSchema,
workflowDeleteRecordActionSettingsSchema,
workflowFindRecordsActionSettingsSchema,
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowAiAgentActionSettingsSchema,
workflowFilterActionSettingsSchema,
workflowIteratorActionSettingsSchema,
workflowEmptyActionSettingsSchema,
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema,
workflowFormActionSchema,
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
workflowIteratorActionSchema,
workflowEmptyActionSchema,
workflowActionSchema,
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
workflowWebhookTriggerSchema,
workflowTriggerSchema,
workflowRunStepStatusSchema,
workflowRunStateStepInfoSchema,
workflowRunStateStepInfosSchema,
workflowRunStateSchema,
workflowRunStatusSchema,
workflowRunSchema,
} from './schemas/workflow.schema';
export type { BodyType } from './types/workflowHttpRequestStep';
export type {
WorkflowRunStepInfo,

239
yarn.lock
View file

@ -24,118 +24,86 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/anthropic@npm:^1.2.12":
version: 1.2.12
resolution: "@ai-sdk/anthropic@npm:1.2.12"
"@ai-sdk/anthropic@npm:^2.0.17":
version: 2.0.17
resolution: "@ai-sdk/anthropic@npm:2.0.17"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.8"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
peerDependencies:
zod: ^3.0.0
checksum: 10c0/da13e1ed3c03efe207dbb0fd5fe9f399e4119e6687ec1096418a33a7eeea3c5f912a51c74b185bba3c203b15ee0c1b9cdf649711815ff8e769e31af266ac00fb
zod: ^3.25.76 || ^4
checksum: 10c0/783b6a953f3854c4303ad7c30dd56d4706486c7d1151adb17071d87933418c59c26bce53d5c26d34c4d4728eaac4a856ce49a336caed26a7216f982fea562814
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:0.2.13":
version: 0.2.13
resolution: "@ai-sdk/openai-compatible@npm:0.2.13"
"@ai-sdk/gateway@npm:1.0.23":
version: 1.0.23
resolution: "@ai-sdk/gateway@npm:1.0.23"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.7"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
peerDependencies:
zod: ^3.0.0
checksum: 10c0/d06d46c319f5b846339aa3ba6144e27791fba4eaa7ba47a758cec52b0c9e578fae61a2ab5cc1f52adaf010a36b3c9309ef46c8ba5de77bea01d15d2d4eb7c27a
zod: ^3.25.76 || ^4
checksum: 10c0/b1e1a6ab63b9191075eed92c586cd927696f8997ad24f056585aee3f5fffd283d981aa6b071a2560ecda4295445b80a4cfd321fa63c06e7ac54a06bc4c84887f
languageName: node
linkType: hard
"@ai-sdk/openai@npm:^1.3.22":
version: 1.3.22
resolution: "@ai-sdk/openai@npm:1.3.22"
"@ai-sdk/openai-compatible@npm:1.0.18":
version: 1.0.18
resolution: "@ai-sdk/openai-compatible@npm:1.0.18"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.8"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
peerDependencies:
zod: ^3.0.0
checksum: 10c0/bcc73a84bebd15aa54568c3c77cedd5f999e282c5be180d5e28ebc789f8873dd0a74d87f1ec4a0f16e3e61b658c3b0734835daf176ed910966246db73c72b468
zod: ^3.25.76 || ^4
checksum: 10c0/6fa84f6f1be07d13b1875c1ab3b009d0e659423dc9fc1c140bd0b3b99457356173c3b2d3289c535ac1abb542f7d56ff0a8538561a3e78c4b0bfa2de9d8a3e81c
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:2.2.7":
version: 2.2.7
resolution: "@ai-sdk/provider-utils@npm:2.2.7"
"@ai-sdk/openai@npm:^2.0.30":
version: 2.0.30
resolution: "@ai-sdk/openai@npm:2.0.30"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
nanoid: "npm:^3.3.8"
secure-json-parse: "npm:^2.7.0"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
peerDependencies:
zod: ^3.23.8
checksum: 10c0/e89a4e03be59df56bfb15e25e761955ffabd39b350527dd3e27da89c35332d1db6eeffc596d2aa3e18a2f5535d79e8ddc4ad7066d6f05f490f7d10082f427f00
zod: ^3.25.76 || ^4
checksum: 10c0/90a57c1b10dac46c0bbe7e16cf9202557fb250d9f0e94a2a5fb7d95b5ea77815a56add78b00238d3823f0313c9b2c42abe865478d28a6196f72b341d32dd40af
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:2.2.8":
version: 2.2.8
resolution: "@ai-sdk/provider-utils@npm:2.2.8"
"@ai-sdk/provider-utils@npm:3.0.9, @ai-sdk/provider-utils@npm:^3.0.9":
version: 3.0.9
resolution: "@ai-sdk/provider-utils@npm:3.0.9"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
nanoid: "npm:^3.3.8"
secure-json-parse: "npm:^2.7.0"
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.23.8
checksum: 10c0/34c72bf5f23f2d3e7aef496da7099422ba3b3ff243c35511853e16c3f1528717500262eea32b19e3e09bc4452152a5f31e650512f53f08a5f5645d907bff429e
zod: ^3.25.76 || ^4
checksum: 10c0/f8b659343d7e22ae099f7b6fc514591c0408012eb0aa00f7a912798b6d7d7305cafa8f18a07c7adec0bb5d39d9b6256b76d65c5393c3fc843d1361c52f1f8080
languageName: node
linkType: hard
"@ai-sdk/provider@npm:1.1.3":
version: 1.1.3
resolution: "@ai-sdk/provider@npm:1.1.3"
"@ai-sdk/provider@npm:2.0.0":
version: 2.0.0
resolution: "@ai-sdk/provider@npm:2.0.0"
dependencies:
json-schema: "npm:^0.4.0"
checksum: 10c0/40e080e223328e7c89829865e9c48f4ce8442a6a59f7ed5dfbdb4f63e8d859a76641e2d31e91970dd389bddb910f32ec7c3dbb0ce583c119e5a1e614ea7b8bc4
checksum: 10c0/e50e520016c9fc0a8b5009cadd47dae2f1c81ec05c1792b9e312d7d15479f024ca8039525813a33425c884e3449019fed21043b1bfabd6a2626152ca9a388199
languageName: node
linkType: hard
"@ai-sdk/react@npm:1.2.12":
version: 1.2.12
resolution: "@ai-sdk/react@npm:1.2.12"
"@ai-sdk/xai@npm:^2.0.19":
version: 2.0.19
resolution: "@ai-sdk/xai@npm:2.0.19"
dependencies:
"@ai-sdk/provider-utils": "npm:2.2.8"
"@ai-sdk/ui-utils": "npm:1.2.11"
swr: "npm:^2.2.5"
throttleit: "npm:2.1.0"
"@ai-sdk/openai-compatible": "npm:1.0.18"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
peerDependenciesMeta:
zod:
optional: true
checksum: 10c0/5422feb4ffeebd3287441cf658733e9ad7f9081fc279e85f57700d7fe9f4ed8a0504789c1be695790df44b28730e525cf12acf0f52bfa5adecc561ffd00cb2a5
languageName: node
linkType: hard
"@ai-sdk/ui-utils@npm:1.2.11":
version: 1.2.11
resolution: "@ai-sdk/ui-utils@npm:1.2.11"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.8"
zod-to-json-schema: "npm:^3.24.1"
peerDependencies:
zod: ^3.23.8
checksum: 10c0/de0a10f9e16010126a21a1690aaf56d545b9c0f8d8b2cc33ffd22c2bb2e914949acb9b3f86e0e39a0e4b0d4f24db12e2b094045e34b311de0c8f84bfab48cc92
languageName: node
linkType: hard
"@ai-sdk/xai@npm:1.2.15":
version: 1.2.15
resolution: "@ai-sdk/xai@npm:1.2.15"
dependencies:
"@ai-sdk/openai-compatible": "npm:0.2.13"
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.7"
peerDependencies:
zod: ^3.0.0
checksum: 10c0/60bd4af83dd90e4b6b3a4149633c77bcfb11e976013f86fd3383f8f23f5fb3afa1d790607fefc34d6a33cfd80fd9680b5b01366a6016070792ab8055b204a55c
zod: ^3.25.76 || ^4
checksum: 10c0/3a9aa297c98ea3f6a7305ab9c37c68a1e661f4358ef14668a82db27437334aa01aa632ebf656cd1770c221860ab6153a86363301387fab4acedeca10b6b8026e
languageName: node
linkType: hard
@ -17967,6 +17935,13 @@ __metadata:
languageName: node
linkType: hard
"@standard-schema/spec@npm:^1.0.0":
version: 1.0.0
resolution: "@standard-schema/spec@npm:1.0.0"
checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f
languageName: node
linkType: hard
"@stitches/core@npm:^1.2.6":
version: 1.2.8
resolution: "@stitches/core@npm:1.2.8"
@ -20037,13 +20012,6 @@ __metadata:
languageName: node
linkType: hard
"@types/diff-match-patch@npm:^1.0.36":
version: 1.0.36
resolution: "@types/diff-match-patch@npm:1.0.36"
checksum: 10c0/0bad011ab138baa8bde94e7815064bb881f010452463272644ddbbb0590659cb93f7aa2776ff442c6721d70f202839e1053f8aa62d801cc4166f7a3ea9130055
languageName: node
linkType: hard
"@types/doctrine@npm:^0.0.9":
version: 0.0.9
resolution: "@types/doctrine@npm:0.0.9"
@ -23026,23 +22994,17 @@ __metadata:
languageName: node
linkType: hard
"ai@npm:^4.3.16":
version: 4.3.16
resolution: "ai@npm:4.3.16"
"ai@npm:^5.0.44":
version: 5.0.44
resolution: "ai@npm:5.0.44"
dependencies:
"@ai-sdk/provider": "npm:1.1.3"
"@ai-sdk/provider-utils": "npm:2.2.8"
"@ai-sdk/react": "npm:1.2.12"
"@ai-sdk/ui-utils": "npm:1.2.11"
"@ai-sdk/gateway": "npm:1.0.23"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.9"
"@opentelemetry/api": "npm:1.9.0"
jsondiffpatch: "npm:0.6.0"
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
peerDependenciesMeta:
react:
optional: true
checksum: 10c0/befe761c9386cda6de33370a2590900352b444d81959255c624e2bfd40765f126d29269f0ef3e00bde07daf237004aa0b66d0b253664aa478c148e923ce78c41
zod: ^3.25.76 || ^4
checksum: 10c0/528c7e165f75715194204051ce0aa341d8dca7d5536c2abcf3df83ccda7399ed5d91deaa45a81340f93d2461b1c2fc5f740f7804dfd396927c71b0667403569b
languageName: node
linkType: hard
@ -29354,13 +29316,6 @@ __metadata:
languageName: node
linkType: hard
"diff-match-patch@npm:^1.0.5":
version: 1.0.5
resolution: "diff-match-patch@npm:1.0.5"
checksum: 10c0/142b6fad627b9ef309d11bd935e82b84c814165a02500f046e2773f4ea894d10ed3017ac20454900d79d4a0322079f5b713cf0986aaf15fce0ec4a2479980c86
languageName: node
linkType: hard
"diff-sequences@npm:^29.6.3":
version: 29.6.3
resolution: "diff-sequences@npm:29.6.3"
@ -31600,6 +31555,13 @@ __metadata:
languageName: node
linkType: hard
"eventsource-parser@npm:^3.0.5":
version: 3.0.6
resolution: "eventsource-parser@npm:3.0.6"
checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a
languageName: node
linkType: hard
"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3":
version: 1.0.3
resolution: "evp_bytestokey@npm:1.0.3"
@ -38370,19 +38332,6 @@ __metadata:
languageName: node
linkType: hard
"jsondiffpatch@npm:0.6.0":
version: 0.6.0
resolution: "jsondiffpatch@npm:0.6.0"
dependencies:
"@types/diff-match-patch": "npm:^1.0.36"
chalk: "npm:^5.3.0"
diff-match-patch: "npm:^1.0.5"
bin:
jsondiffpatch: bin/jsondiffpatch.js
checksum: 10c0/f7822e48a8ef8b9f7c6024cc59b7d3707a9fe6d84fd776d169de5a1803ad551ffe7cfdc7587f3900f224bc70897355884ed43eb1c8ccd02e7f7b43a7ebcfed4f
languageName: node
linkType: hard
"jsonfile@npm:^4.0.0":
version: 4.0.0
resolution: "jsonfile@npm:4.0.0"
@ -42296,7 +42245,7 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8":
"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
bin:
@ -48408,13 +48357,6 @@ __metadata:
languageName: node
linkType: hard
"secure-json-parse@npm:^2.7.0":
version: 2.7.0
resolution: "secure-json-parse@npm:2.7.0"
checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4
languageName: node
linkType: hard
"selderee@npm:^0.11.0":
version: 0.11.0
resolution: "selderee@npm:0.11.0"
@ -50510,18 +50452,6 @@ __metadata:
languageName: node
linkType: hard
"swr@npm:^2.2.5":
version: 2.3.3
resolution: "swr@npm:2.3.3"
dependencies:
dequal: "npm:^2.0.3"
use-sync-external-store: "npm:^1.4.0"
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/882fc8291912860e0c50eae3470ebf0cd58b0144cb12adcc4b14c5cef913ea06479043830508d8b0b3d4061d99ad8dd52485c9c879fbd4e9b893484e6d8da9e3
languageName: node
linkType: hard
"symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0":
version: 4.0.0
resolution: "symbol-observable@npm:4.0.0"
@ -50772,13 +50702,6 @@ __metadata:
languageName: node
linkType: hard
"throttleit@npm:2.1.0":
version: 2.1.0
resolution: "throttleit@npm:2.1.0"
checksum: 10c0/1696ae849522cea6ba4f4f3beac1f6655d335e51b42d99215e196a718adced0069e48deaaf77f7e89f526ab31de5b5c91016027da182438e6f9280be2f3d5265
languageName: node
linkType: hard
"through2@npm:4.0.2, through2@npm:^4.0.2":
version: 4.0.2
resolution: "through2@npm:4.0.2"
@ -51760,9 +51683,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "twenty-server@workspace:packages/twenty-server"
dependencies:
"@ai-sdk/anthropic": "npm:^1.2.12"
"@ai-sdk/openai": "npm:^1.3.22"
"@ai-sdk/xai": "npm:1.2.15"
"@ai-sdk/anthropic": "npm:^2.0.17"
"@ai-sdk/openai": "npm:^2.0.30"
"@ai-sdk/provider-utils": "npm:^3.0.9"
"@ai-sdk/xai": "npm:^2.0.19"
"@aws-sdk/client-lambda": "npm:3.825.0"
"@aws-sdk/client-s3": "npm:3.825.0"
"@aws-sdk/client-sts": "npm:3.825.0"
@ -51860,7 +51784,7 @@ __metadata:
"@types/unzipper": "npm:^0"
"@yarnpkg/types": "npm:^4.0.0"
addressparser: "npm:1.0.1"
ai: "npm:^4.3.16"
ai: "npm:^5.0.44"
apollo-server-core: "npm:3.13.0"
archiver: "npm:7.0.1"
axios: "npm:1.10.0"
@ -51972,7 +51896,7 @@ __metadata:
unzipper: "npm:^0.12.3"
uuid: "npm:9.0.1"
vite-tsconfig-paths: "npm:4.3.2"
zod: "npm:3.23.8"
zod: "npm:^4.1.11"
zod-to-json-schema: "npm:^3.23.1"
languageName: unknown
linkType: soft
@ -55484,7 +55408,7 @@ __metadata:
languageName: node
linkType: hard
"zod-to-json-schema@npm:^3.23.1, zod-to-json-schema@npm:^3.24.1":
"zod-to-json-schema@npm:^3.23.1":
version: 3.24.5
resolution: "zod-to-json-schema@npm:3.24.5"
peerDependencies:
@ -55507,6 +55431,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^4.1.11":
version: 4.1.11
resolution: "zod@npm:4.1.11"
checksum: 10c0/ce6a4c4acfbf51d7dd0f2669c82f207d62a1f00264eef608994b94eb99d86a74c99f59b0dd3e61ef82909ee136631378b709e0908f0a02a2d5c21d0c497de5db
languageName: node
linkType: hard
"zustand@npm:^4.4.0":
version: 4.5.4
resolution: "zustand@npm:4.5.4"