Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
97b19f0
fix: items
scopsy Apr 14, 2026
2ef2af2
Update agent-sidebar-widget.tsx
scopsy Apr 14, 2026
46b25a7
fix: item
scopsy Apr 14, 2026
4e65c8c
setup agentguide
scopsy Apr 14, 2026
a8adbc9
Update agent-setup-guide.tsx
scopsy Apr 14, 2026
e35d135
Add provider dropdown and wire into setup guide
scopsy Apr 14, 2026
dad1609
feat: add supported providers
scopsy Apr 14, 2026
1bfe0bf
Update provider-dropdown.tsx
scopsy Apr 14, 2026
6afeb25
Update Slack manifest and provider dropdown
scopsy Apr 14, 2026
e24e085
Update agent-setup-guide.tsx
scopsy Apr 14, 2026
174cafd
Update agent-setup-guide.tsx
scopsy Apr 14, 2026
bff3132
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 14, 2026
aea6cc6
Avoid linking already-added agent integrations
scopsy Apr 14, 2026
e42b0c6
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 14, 2026
8a86b84
Add agent 'active' flag and integrations UI improvements
scopsy Apr 14, 2026
a89cce3
Update agent-setup-guide.tsx
scopsy Apr 15, 2026
ca305ae
fix:
scopsy Apr 15, 2026
31a4347
Refactor agent polling/confetti; handle link conflicts
scopsy Apr 15, 2026
543aecf
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 15, 2026
eb67bf5
Update agent-setup-guide.tsx
scopsy Apr 15, 2026
d2957ab
fix
scopsy Apr 15, 2026
eba5a0e
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 15, 2026
55d3fb9
Update slack-setup-guide.tsx
scopsy Apr 15, 2026
bfb4417
Update slack-setup-guide.tsx
scopsy Apr 15, 2026
58b2050
Handle missing bridge URL with onboarding reply
scopsy Apr 15, 2026
20a83ff
Update slack-setup-guide.tsx
scopsy Apr 15, 2026
f445381
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 15, 2026
6c4f377
Improve agents, provider UI, and Slack setup
scopsy Apr 15, 2026
3e0cb33
Merge branch 'next' into agent-overview-page-impl
scopsy Apr 15, 2026
5a82d83
fix:
scopsy Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions apps/api/src/app/agents/agents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ import { ListAgentsCommand } from './usecases/list-agents/list-agents.command';
import { ListAgents } from './usecases/list-agents/list-agents.usecase';
import { RemoveAgentIntegrationCommand } from './usecases/remove-agent-integration/remove-agent-integration.command';
import { RemoveAgentIntegration } from './usecases/remove-agent-integration/remove-agent-integration.usecase';
import { UpdateAgentIntegrationCommand } from './usecases/update-agent-integration/update-agent-integration.command';
import { UpdateAgentIntegration } from './usecases/update-agent-integration/update-agent-integration.usecase';
import { UpdateAgentCommand } from './usecases/update-agent/update-agent.command';
import { UpdateAgent } from './usecases/update-agent/update-agent.usecase';
import { UpdateAgentIntegrationCommand } from './usecases/update-agent-integration/update-agent-integration.command';
import { UpdateAgentIntegration } from './usecases/update-agent-integration/update-agent-integration.usecase';

@ThrottlerCategory(ApiRateLimitCategoryEnum.CONFIGURATION)
@ApiCommonResponses()
Expand Down Expand Up @@ -84,10 +84,7 @@ export class AgentsController {
description: 'Creates an agent scoped to the current environment. The identifier must be unique per environment.',
})
@RequirePermissions(PermissionsEnum.AGENT_WRITE)
createAgent(
@UserSession() user: UserSessionData,
@Body() body: CreateAgentRequestDto
): Promise<AgentResponseDto> {
createAgent(@UserSession() user: UserSessionData, @Body() body: CreateAgentRequestDto): Promise<AgentResponseDto> {
return this.createAgentUsecase.execute(
CreateAgentCommand.create({
userId: user._id,
Expand All @@ -96,6 +93,7 @@ export class AgentsController {
name: body.name,
identifier: body.identifier,
description: body.description,
active: body.active,
})
);
}
Expand All @@ -108,10 +106,7 @@ export class AgentsController {
'Returns a cursor-paginated list of agents for the current environment. Use **after**, **before**, **limit**, **orderBy**, and **orderDirection** query parameters.',
})
@RequirePermissions(PermissionsEnum.AGENT_READ)
listAgents(
@UserSession() user: UserSessionData,
@Query() query: ListAgentsQueryDto
): Promise<ListAgentsResponseDto> {
listAgents(@UserSession() user: UserSessionData, @Query() query: ListAgentsQueryDto): Promise<ListAgentsResponseDto> {
return this.listAgentsUsecase.execute(
ListAgentsCommand.create({
user,
Expand All @@ -132,7 +127,8 @@ export class AgentsController {
@ApiResponse(AgentIntegrationResponseDto, 201)
@ApiOperation({
summary: 'Link integration to agent',
description: 'Creates a link between an agent (by identifier) and an integration (by integration **identifier**, not the internal _id).',
description:
'Creates a link between an agent (by identifier) and an integration (by integration **identifier**, not the internal _id).',
})
@ApiNotFoundResponse({
description: 'The agent or integration was not found.',
Expand Down Expand Up @@ -287,6 +283,7 @@ export class AgentsController {
identifier,
name: body.name,
description: body.description,
active: body.active,
behavior: body.behavior,
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ChannelTypeEnum } from '@novu/shared';

/** Picked integration fields embedded on an agent–integration link response. */
Expand Down Expand Up @@ -40,6 +40,9 @@ export class AgentIntegrationResponseDto {
@ApiProperty()
_organizationId: string;

@ApiPropertyOptional({ description: 'Set when the agent–integration link has been used (e.g. first credential resolution).' })
connectedAt?: string | null;

@ApiProperty()
createdAt: string;

Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/app/agents/dtos/agent-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class AgentResponseDto {
@ApiPropertyOptional({ type: AgentBehaviorDto })
behavior?: AgentBehaviorDto;

@ApiProperty()
active: boolean;

@ApiProperty()
_environmentId: string;

Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/app/agents/dtos/create-agent-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SLUG_IDENTIFIER_REGEX, slugIdentifierFormatMessage } from '@novu/shared';
import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';

export class CreateAgentRequestDto {
@ApiProperty()
Expand All @@ -20,4 +20,9 @@ export class CreateAgentRequestDto {
@IsString()
@IsOptional()
description?: string;

@ApiPropertyOptional({ default: true })
@IsBoolean()
@IsOptional()
active?: boolean;
}
7 changes: 6 additions & 1 deletion apps/api/src/app/agents/dtos/update-agent-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator';

import { AgentBehaviorDto } from './agent-behavior.dto';

Expand All @@ -15,6 +15,11 @@ export class UpdateAgentRequestDto {
@IsOptional()
description?: string;

@ApiPropertyOptional()
@IsBoolean()
@IsOptional()
active?: boolean;

@ApiPropertyOptional({ type: AgentBehaviorDto })
@ValidateNested()
@Type(() => AgentBehaviorDto)
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/agents/mappers/agent-response.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function toAgentResponse(agent: AgentEntity): AgentResponseDto {
name: agent.name,
identifier: agent.identifier,
description: agent.description,
active: agent.active,
behavior: agent.behavior,
_environmentId: agent._environmentId,
_organizationId: agent._organizationId,
Expand All @@ -33,7 +34,6 @@ export function toAgentIntegrationResponse(
link: AgentIntegrationEntity,
integration: Pick<IntegrationEntity, '_id' | 'identifier' | 'name' | 'providerId' | 'channel' | 'active'>
): AgentIntegrationResponseDto {

return {
_id: link._id,
_agentId: link._agentId,
Expand All @@ -47,6 +47,7 @@ export function toAgentIntegrationResponse(
},
_environmentId: link._environmentId,
_organizationId: link._organizationId,
connectedAt: link.connectedAt ?? null,
createdAt: link.createdAt,
updatedAt: link.updatedAt,
};
Expand Down
16 changes: 14 additions & 2 deletions apps/api/src/app/agents/services/agent-credential.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
import { decryptCredentials, FeatureFlagsService } from '@novu/application-generic';
import { decryptCredentials, FeatureFlagsService, PinoLogger } from '@novu/application-generic';
import {
AgentIntegrationRepository,
AgentRepository,
Expand Down Expand Up @@ -34,7 +34,8 @@ export class AgentCredentialService {
private readonly agentRepository: AgentRepository,
private readonly agentIntegrationRepository: AgentIntegrationRepository,
private readonly integrationRepository: IntegrationRepository,
private readonly channelConnectionRepository: ChannelConnectionRepository
private readonly channelConnectionRepository: ChannelConnectionRepository,
private readonly logger: PinoLogger
) {}

async resolve(agentId: string, integrationIdentifier: string): Promise<ResolvedPlatformConfig> {
Expand Down Expand Up @@ -77,6 +78,17 @@ export class AgentCredentialService {
throw new UnprocessableEntityException(`Agent ${agentId} is not linked to integration ${integrationIdentifier}`);
}

if (agentIntegration.connectedAt == null) {
await this.agentIntegrationRepository.updateOne(
{
_id: agentIntegration._id,
_environmentId: environmentId,
_organizationId: organizationId,
},
{ $set: { connectedAt: new Date() } }
);
}
Comment thread
scopsy marked this conversation as resolved.
Outdated

const platform = resolveAgentPlatform(integration.providerId);
if (!platform) {
throw new UnprocessableEntityException(
Expand Down
62 changes: 45 additions & 17 deletions apps/api/src/app/agents/services/agent-inbound-handler.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Injectable } from '@nestjs/common';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { PinoLogger } from '@novu/application-generic';
import { ConversationActivitySenderTypeEnum, ConversationParticipantTypeEnum, SubscriberRepository } from '@novu/dal';
import type { Message, Thread } from 'chat';
import { AgentEventEnum } from '../dtos/agent-event.enum';
import { ResolvedPlatformConfig } from './agent-credential.service';
import { HandleAgentReplyCommand } from '../usecases/handle-agent-reply/handle-agent-reply.command';
import { HandleAgentReply } from '../usecases/handle-agent-reply/handle-agent-reply.usecase';
import { AgentConversationService } from './agent-conversation.service';
import { ResolvedPlatformConfig } from './agent-credential.service';
import { AgentSubscriberResolver } from './agent-subscriber-resolver.service';
import { BridgeExecutorService } from './bridge-executor.service';
import { BridgeExecutorService, NoBridgeUrlError } from './bridge-executor.service';

const ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN = `*You're connected to Novu*

Your bot is linked successfully. Go back to the *Novu dashboard* to complete onboarding.`;

@Injectable()
export class AgentInboundHandler {
Expand All @@ -15,7 +21,9 @@ export class AgentInboundHandler {
private readonly subscriberResolver: AgentSubscriberResolver,
private readonly conversationService: AgentConversationService,
private readonly bridgeExecutor: BridgeExecutorService,
private readonly subscriberRepository: SubscriberRepository
private readonly subscriberRepository: SubscriberRepository,
@Inject(forwardRef(() => HandleAgentReply))
private readonly handleAgentReply: HandleAgentReply
) {}

async handle(
Expand Down Expand Up @@ -95,18 +103,38 @@ export class AgentInboundHandler {
this.conversationService.getHistory(config.environmentId, conversation._id),
]);

await this.bridgeExecutor.execute({
event,
config,
conversation,
subscriber,
history,
message,
platformContext: {
threadId: thread.id,
channelId: thread.channelId,
isDM: thread.isDM,
},
});
try {
await this.bridgeExecutor.execute({
event,
config,
conversation,
subscriber,
history,
message,
platformContext: {
threadId: thread.id,
channelId: thread.channelId,
isDM: thread.isDM,
},
});
} catch (err) {
if (err instanceof NoBridgeUrlError) {
await this.handleAgentReply.execute(
HandleAgentReplyCommand.create({
userId: 'system',
environmentId: config.environmentId,
organizationId: config.organizationId,
conversationId: conversation._id,
agentIdentifier: agentId,
integrationIdentifier: config.integrationIdentifier,
reply: { text: ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN },
})
);

return;
}

throw err;
}
}
}
13 changes: 12 additions & 1 deletion apps/api/src/app/agents/services/bridge-executor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ export interface AgentBridgeRequest {
platformContext: BridgePlatformContext;
}

export class NoBridgeUrlError extends Error {
constructor(agentIdentifier: string) {
super(`No bridge URL configured for agent ${agentIdentifier}`);
this.name = 'NoBridgeUrlError';
}
}

@Injectable()
export class BridgeExecutorService {
constructor(
Expand All @@ -112,7 +119,7 @@ export class BridgeExecutorService {

const bridgeUrl = await this.resolveBridgeUrl(config.environmentId, config.organizationId, agentIdentifier, event);
if (!bridgeUrl) {
return;
throw new NoBridgeUrlError(agentIdentifier);
}

const secretKey = await this.getDecryptedSecretKey.execute(
Expand All @@ -126,6 +133,10 @@ export class BridgeExecutorService {
this.logger.error(err, `[agent:${agentIdentifier}] Bridge delivery failed after ${MAX_RETRIES + 1} attempts`);
});
} catch (err) {
if (err instanceof NoBridgeUrlError) {
throw err;
}

this.logger.error(err, `[agent:${agentIdentifier}] Bridge setup failed — skipping bridge call`);
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/agents/services/chat-sdk.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, OnModuleDestroy } from '@nestjs/common';
import { BadRequestException, forwardRef, Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import { PinoLogger } from '@novu/application-generic';
import type { Chat, Message, Thread } from 'chat';
import { Request as ExpressRequest, Response as ExpressResponse } from 'express';
Expand Down Expand Up @@ -41,6 +41,7 @@ export class ChatSdkService implements OnModuleDestroy {
constructor(
private readonly logger: PinoLogger,
private readonly agentCredentialService: AgentCredentialService,
@Inject(forwardRef(() => AgentInboundHandler))
private readonly inboundHandler: AgentInboundHandler
) {
this.instances = new LRUCache<string, Chat>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';

import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';

Expand All @@ -14,4 +14,8 @@ export class CreateAgentCommand extends EnvironmentWithUserCommand {
@IsString()
@IsOptional()
description?: string;

@IsBoolean()
@IsOptional()
active?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class CreateAgent {
name: command.name,
identifier: command.identifier,
description: command.description,
active: command.active ?? true,
_environmentId: command.environmentId,
_organizationId: command.organizationId,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { PinoLogger, shortId } from '@novu/application-generic';
import {
ConversationActivityRepository,
Expand All @@ -22,6 +22,7 @@ export class HandleAgentReply {
private readonly conversationRepository: ConversationRepository,
private readonly activityRepository: ConversationActivityRepository,
private readonly subscriberRepository: SubscriberRepository,
@Inject(forwardRef(() => ChatSdkService))
private readonly chatSdkService: ChatSdkService,
private readonly bridgeExecutor: BridgeExecutorService,
private readonly agentCredentialService: AgentCredentialService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator';

import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
import { AgentBehaviorDto } from '../../dtos/agent-behavior.dto';
Expand All @@ -17,6 +17,10 @@ export class UpdateAgentCommand extends EnvironmentWithUserCommand {
@IsOptional()
description?: string;

@ValidateIf((_, value) => value !== undefined)
@IsBoolean()
active?: boolean;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@ValidateNested()
@Type(() => AgentBehaviorDto)
@IsOptional()
Expand Down
Loading
Loading