[SDK] Agent in manifest (#18431)

# Introduction
Adding agent in the manifest, required for twenty standard app
extraction out of twenty-server
This commit is contained in:
Paul Rastoin
2026-03-09 12:10:14 +01:00
committed by GitHub
parent a9696705c1
commit 06bdb5ad6a
24 changed files with 476 additions and 5 deletions
@@ -113,6 +113,7 @@ export class CreateAppCommand {
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleIntegrationTest: false,
};
}
@@ -126,6 +127,7 @@ export class CreateAppCommand {
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleIntegrationTest: true,
includeExampleAgent: true,
};
}
@@ -8,5 +8,6 @@ export type ExampleOptions = {
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
includeExampleAgent: boolean;
includeExampleIntegrationTest: boolean;
};
@@ -25,6 +25,7 @@ const ALL_EXAMPLES: ExampleOptions = {
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleAgent: true,
includeExampleIntegrationTest: true,
};
@@ -41,6 +42,7 @@ const NO_EXAMPLES: ExampleOptions = {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
@@ -476,6 +478,7 @@ describe('copyBaseApplicationProject', () => {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: true,
includeExampleView: false,
@@ -513,6 +516,7 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: {
includeExampleObject: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleField: false,
includeExampleLogicFunction: true,
includeExampleFrontComponent: false,
@@ -103,6 +103,14 @@ export const copyBaseApplicationProject = async ({
});
}
if (exampleOptions.includeExampleAgent) {
await createExampleAgent({
appDirectory: sourceFolderPath,
fileFolder: 'agents',
fileName: 'example-agent.ts',
});
}
if (exampleOptions.includeExampleIntegrationTest) {
await scaffoldIntegrationTest({
appDirectory,
@@ -519,6 +527,36 @@ export default defineSkill({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleAgent = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineAgent } from 'twenty-sdk';
export const EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineAgent({
universalIdentifier: EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER,
name: 'example-agent',
label: 'Example Agent',
description: 'A sample AI agent for your application',
icon: 'IconRobot',
prompt: 'You are a helpful assistant. Help users with their questions and tasks.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -14,7 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
**What you can do today:**
- Define custom objects and fields as code (managed data model)
- Build logic functions with custom triggers
- Define skills for AI agents
- Define skills and agents for AI
- Deploy the same app across multiple workspaces
## Prerequisites
@@ -38,7 +38,7 @@ yarn twenty app:dev
The scaffolder supports two modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -114,8 +114,10 @@ my-twenty-app/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
── skills/
└── example-skill.ts # Example AI agent skill definition
── skills/
└── example-skill.ts # Example AI agent skill definition
└── agents/
└── example-agent.ts # Example AI agent definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`).
@@ -147,6 +149,7 @@ The SDK detects entities by parsing your TypeScript files for **`export default
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
| `defineAgent()` | AI agent definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
@@ -223,6 +226,7 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
| `defineAgent()` | Define AI agents with system prompts |
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
@@ -790,6 +794,40 @@ You can create new skills in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Agents
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
```typescript
// src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';
export default defineAgent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt: `You are a sales assistant. Help users with:
1. Researching prospects and companies
2. Drafting personalized outreach messages
3. Tracking follow-ups and next steps
4. Analyzing deal pipeline and suggesting actions`,
});
```
Key points:
- `name` is a unique identifier string for the agent (kebab-case recommended).
- `label` is the human-readable display name shown in the UI.
- `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
- `icon` (optional) sets the icon displayed in the UI.
- `description` (optional) provides additional context about the agent's purpose.
You can create new agents in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
- **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
### Generated typed clients
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
@@ -19,6 +19,7 @@ export const EXPECTED_MANIFEST: Manifest = {
},
],
skills: [],
agents: [],
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
@@ -18,6 +18,7 @@ export const EXPECTED_MANIFEST: Manifest = {
apiClientChecksum: null,
},
skills: [],
agents: [],
publicAssets: [],
fields: [
{
@@ -16,6 +16,7 @@ import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-nav
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import { getAgentBaseFile } from '@/cli/utilities/entity/entity-agent-template';
import { getSkillBaseFile } from '@/cli/utilities/entity/entity-skill-template';
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
import { ensureDir, pathExists } from '@/cli/utilities/file/fs-utils';
@@ -144,6 +145,16 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.Agent: {
const name = await this.getEntityName(entity);
const file = getAgentBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.View: {
const entityData = await this.getViewData();
@@ -33,6 +33,7 @@ const validManifest: Manifest = {
logicFunctions: [],
roles: [],
skills: [],
agents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
@@ -19,6 +19,7 @@ import { readFile } from 'node:fs/promises';
import { basename, extname, relative } from 'path';
import { glob } from 'tinyglobby';
import {
type AgentManifest,
type ApplicationManifest,
type AssetManifest,
ASSETS_DIR,
@@ -68,6 +69,7 @@ export const buildManifest = async (
const fields: FieldManifest[] = [];
const roles: RoleManifest[] = [];
const skills: SkillManifest[] = [];
const agents: AgentManifest[] = [];
const logicFunctions: LogicFunctionManifest[] = [];
const frontComponents: FrontComponentManifest[] = [];
const publicAssets: AssetManifest[] = [];
@@ -82,6 +84,7 @@ export const buildManifest = async (
const fieldsFilePaths: string[] = [];
const rolesFilePaths: string[] = [];
const skillsFilePaths: string[] = [];
const agentsFilePaths: string[] = [];
const logicFunctionsFilePaths: string[] = [];
const frontComponentsFilePaths: string[] = [];
const publicAssetsFilePaths: string[] = [];
@@ -184,6 +187,16 @@ export const buildManifest = async (
skillsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Agents: {
const extract = await extractManifestFromFile<AgentManifest>({
appPath,
filePath,
});
agents.push(extract.config);
errors.push(...extract.errors);
agentsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.LogicFunctions: {
const extract = await extractManifestFromFile<LogicFunctionConfig>({
appPath,
@@ -363,6 +376,7 @@ export const buildManifest = async (
fields,
roles,
skills,
agents,
logicFunctions,
frontComponents,
publicAssets,
@@ -377,6 +391,7 @@ export const buildManifest = async (
fields: fieldsFilePaths,
roles: rolesFilePaths,
skills: skillsFilePaths,
agents: agentsFilePaths,
logicFunctions: logicFunctionsFilePaths,
frontComponents: frontComponentsFilePaths,
publicAssets: publicAssetsFilePaths,
@@ -9,6 +9,7 @@ export enum TargetFunction {
DefineObject = 'defineObject',
DefineRole = 'defineRole',
DefineSkill = 'defineSkill',
DefineAgent = 'defineAgent',
DefineFrontComponent = 'defineFrontComponent',
DefineView = 'defineView',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
@@ -22,6 +23,7 @@ export enum ManifestEntityKey {
Objects = 'objects',
Roles = 'roles',
Skills = 'skills',
Agents = 'agents',
FrontComponents = 'frontComponents',
PublicAssets = 'publicAssets',
Views = 'views',
@@ -45,6 +47,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
[TargetFunction.DefineAgent]: ManifestEntityKey.Agents,
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineNavigationMenuItem]:
@@ -102,6 +102,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.View]: 'Views',
[SyncableEntity.NavigationMenuItem]: 'Navigation menu items',
[SyncableEntity.PageLayout]: 'Page layouts',
[SyncableEntity.Agent]: 'Agents',
};
export const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
@@ -0,0 +1,26 @@
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getAgentBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineAgent } from 'twenty-sdk';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineAgent({
universalIdentifier: ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER,
name: '${kebabCaseName}',
label: '${name}',
description: 'Add a description for your agent',
prompt: 'Add the agent system prompt here',
});
`;
};
@@ -0,0 +1,25 @@
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type AgentManifest } from 'twenty-shared/application';
export const defineAgent: DefineEntity<AgentManifest> = (config) => {
const errors: string[] = [];
if (!config.universalIdentifier) {
errors.push('Agent must have a universalIdentifier');
}
if (!config.name) {
errors.push('Agent must have a name');
}
if (!config.label) {
errors.push('Agent must have a label');
}
if (!config.prompt) {
errors.push('Agent must have a prompt');
}
return createValidationResult({ config, errors });
};
+1
View File
@@ -68,6 +68,7 @@ export { definePageLayout } from './page-layouts/define-page-layout';
export type { PageLayoutConfig } from './page-layouts/page-layout-config';
export { defineRole } from './roles/define-role';
export { PermissionFlag } from './roles/permission-flag-type';
export { defineAgent } from './agents/define-agent';
export { defineSkill } from './skills/define-skill';
export { defineView } from './views/define-view';
export type { ViewConfig } from './views/view-config';
@@ -1,6 +1,5 @@
import { type Manifest } from 'twenty-shared/application';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
@@ -18,6 +17,8 @@ import { fromViewFilterGroupManifestToUniversalFlatViewFilterGroup } from 'src/e
import { fromViewFilterManifestToUniversalFlatViewFilter } from 'src/engine/core-modules/application/application-manifest/converters/from-view-filter-manifest-to-universal-flat-view-filter.util';
import { fromViewGroupManifestToUniversalFlatViewGroup } from 'src/engine/core-modules/application/application-manifest/converters/from-view-group-manifest-to-universal-flat-view-group.util';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromAgentManifestToUniversalFlatAgent } from 'src/engine/core-modules/application/utils/from-agent-manifest-to-universal-flat-agent.util';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
@@ -140,6 +141,17 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
});
}
for (const agentManifest of manifest.agents ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromAgentManifestToUniversalFlatAgent({
agentManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatAgentMaps,
});
}
for (const viewManifest of manifest.views ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromViewManifestToUniversalFlatView({
@@ -7,6 +7,7 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
'frontComponent',
'role',
'skill',
'agent',
'view',
'viewField',
'viewFieldGroup',
@@ -0,0 +1,35 @@
import { type AgentManifest } from 'twenty-shared/application';
import {
DEFAULT_SMART_MODEL,
type ModelId,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
export const fromAgentManifestToUniversalFlatAgent = ({
agentManifest,
applicationUniversalIdentifier,
now,
}: {
agentManifest: AgentManifest;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatAgent => {
return {
universalIdentifier: agentManifest.universalIdentifier,
applicationUniversalIdentifier,
name: agentManifest.name,
label: agentManifest.label,
icon: agentManifest.icon ?? null,
description: agentManifest.description ?? null,
prompt: agentManifest.prompt,
modelId: (agentManifest.modelId as ModelId) ?? DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
modelConfiguration: null,
evaluationInputs: [],
isCustom: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
};
};
@@ -0,0 +1,240 @@
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
import { findAgents } from 'test/integration/metadata/suites/agent/utils/find-agents.util';
import { type Manifest } from 'twenty-shared/application';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_AGENT_ID = uuidv4();
const TEST_SECOND_AGENT_ID = uuidv4();
const AGENT_GQL_FIELDS =
'id name label description prompt icon applicationId modelId';
const buildManifest = (overrides?: Partial<Pick<Manifest, 'agents'>>) =>
buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides });
const findAppAgents = async () => {
const { data } = await findAgents({
gqlFields: AGENT_GQL_FIELDS,
expectToFail: false,
input: undefined,
});
return data.findManyAgents.filter(
(agent) =>
agent.name === 'sales-assistant' || agent.name === 'support-bot',
);
};
describe('Manifest update - agents', () => {
beforeEach(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'App for testing agent manifest updates',
sourcePath: 'test-manifest-update-agent',
});
}, 60000);
afterEach(async () => {
try {
await uninstallApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
} catch {
// May fail if the test didn't fully install/sync
}
await globalThis.testDataSource.query(
`DELETE FROM core."role" WHERE "universalIdentifier" = $1`,
[TEST_ROLE_ID],
);
await globalThis.testDataSource.query(
`DELETE FROM core."file" WHERE "applicationId" IN (
SELECT id FROM core."application" WHERE "universalIdentifier" = $1
)`,
[TEST_APP_ID],
);
await globalThis.testDataSource.query(
`DELETE FROM core."application"
WHERE "universalIdentifier" = $1`,
[TEST_APP_ID],
);
await globalThis.testDataSource.query(
`DELETE FROM core."applicationRegistration"
WHERE "universalIdentifier" = $1`,
[TEST_APP_ID],
);
});
it('should create a new agent when added to manifest on second sync', async () => {
await syncApplication({
manifest: buildManifest({ agents: [] }),
expectToFail: false,
});
const agentsAfterFirstSync = await findAppAgents();
expect(agentsAfterFirstSync).toHaveLength(0);
await syncApplication({
manifest: buildManifest({
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt:
'You are a sales assistant. Help users with prospecting and outreach.',
},
],
}),
expectToFail: false,
});
const agentsAfterSecondSync = await findAppAgents();
expect(agentsAfterSecondSync).toHaveLength(1);
expect(agentsAfterSecondSync[0]).toMatchObject({
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt:
'You are a sales assistant. Help users with prospecting and outreach.',
});
}, 60000);
it('should update an agent when properties change in manifest on second sync', async () => {
await syncApplication({
manifest: buildManifest({
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt:
'You are a sales assistant. Help users with prospecting and outreach.',
},
],
}),
expectToFail: false,
});
const agentsAfterFirstSync = await findAppAgents();
expect(agentsAfterFirstSync).toHaveLength(1);
expect(agentsAfterFirstSync[0]).toMatchObject({
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
});
await syncApplication({
manifest: buildManifest({
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'sales-assistant',
label: 'Senior Sales Assistant',
description: 'An advanced AI agent for enterprise sales',
icon: 'IconRobot',
prompt:
'You are a senior sales assistant specializing in enterprise deals. Help users with complex prospecting, multi-threaded outreach, and deal strategy.',
},
],
}),
expectToFail: false,
});
const agentsAfterSecondSync = await findAppAgents();
expect(agentsAfterSecondSync).toHaveLength(1);
expect(agentsAfterSecondSync[0]).toMatchObject({
name: 'sales-assistant',
label: 'Senior Sales Assistant',
description: 'An advanced AI agent for enterprise sales',
prompt:
'You are a senior sales assistant specializing in enterprise deals. Help users with complex prospecting, multi-threaded outreach, and deal strategy.',
});
}, 60000);
it('should delete an agent when removed from manifest on second sync', async () => {
await syncApplication({
manifest: buildManifest({
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt:
'You are a sales assistant. Help users with prospecting and outreach.',
},
{
universalIdentifier: TEST_SECOND_AGENT_ID,
name: 'support-bot',
label: 'Support Bot',
description: 'An AI agent for customer support',
icon: 'IconHeadset',
prompt:
'You are a customer support agent. Help users resolve their issues.',
},
],
}),
expectToFail: false,
});
const agentsAfterFirstSync = await findAppAgents();
expect(agentsAfterFirstSync).toHaveLength(2);
expect(
agentsAfterFirstSync.find(
(agent) => agent.name === 'sales-assistant',
),
).toBeDefined();
expect(
agentsAfterFirstSync.find((agent) => agent.name === 'support-bot'),
).toBeDefined();
await syncApplication({
manifest: buildManifest({
agents: [
{
universalIdentifier: TEST_AGENT_ID,
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt:
'You are a sales assistant. Help users with prospecting and outreach.',
},
],
}),
expectToFail: false,
});
const agentsAfterSecondSync = await findAppAgents();
expect(agentsAfterSecondSync).toHaveLength(1);
expect(agentsAfterSecondSync[0]).toMatchObject({
name: 'sales-assistant',
label: 'Sales Assistant',
});
expect(
agentsAfterSecondSync.find((agent) => agent.name === 'support-bot'),
).toBeUndefined();
}, 60000);
});
@@ -28,6 +28,7 @@ export const buildBaseManifest = ({
},
],
skills: [],
agents: [],
objects: [],
fields: [],
logicFunctions: [],
@@ -0,0 +1,10 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type AgentManifest = SyncableEntityOptions & {
name: string;
label: string;
icon?: string;
description?: string;
prompt: string;
modelId?: string;
};
@@ -5,6 +5,7 @@ export enum SyncableEntity {
FrontComponent = 'frontComponent',
Role = 'role',
Skill = 'skill',
Agent = 'agent',
View = 'view',
NavigationMenuItem = 'navigationMenuItem',
PageLayout = 'pageLayout',
@@ -7,6 +7,7 @@
* |___/
*/
export type { AgentManifest } from './agentManifestType';
export type { ApplicationManifest } from './applicationType';
export type { ApplicationVariables } from './applicationVariablesType';
export type { AssetManifest } from './assetManifestType';
@@ -1,3 +1,4 @@
import { type AgentManifest } from './agentManifestType';
import { type ApplicationManifest } from './applicationType';
import { type AssetManifest } from './assetManifestType';
import { type FieldManifest } from './fieldManifestType';
@@ -18,6 +19,7 @@ export type Manifest = {
frontComponents: FrontComponentManifest[];
roles: RoleManifest[];
skills: SkillManifest[];
agents: AgentManifest[];
publicAssets: AssetManifest[];
views: ViewManifest[];
navigationMenuItems: NavigationMenuItemManifest[];