Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 24 additions & 10 deletions 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 { BadGatewayException, BadRequestException, Injectable, OnModuleDestroy } from '@nestjs/common';
import { CacheService, decryptCredentials, MailFactory, PinoLogger } from '@novu/application-generic';
import { IntegrationRepository } from '@novu/dal';
import type { SentMessageInfo } from '@novu/framework';
Expand All @@ -14,6 +14,16 @@ import { sendWebResponse, toWebRequest } from '../utils/express-to-web-request';
import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolver.service';
import { AgentInboundHandler } from './agent-inbound-handler.service';

function toDeliveryError(err: unknown): never {
const base = err instanceof Error ? err.message : String(err);
const body = (err as any)?.response?.body;
const detail = Array.isArray(body?.errors) ? body.errors[0]?.message : body?.message;
throw new BadGatewayException({
error: 'delivery_failed',
message: detail ? `${base}: ${detail}` : base,
});
}

/** Ensure a Message-ID value is wrapped in RFC 5322 angle brackets. */
function wrapMsgId(id: string): string {
const trimmed = id.trim();
Expand Down Expand Up @@ -134,15 +144,17 @@ export class ChatSdkService implements OnModuleDestroy {
const adapter = chat.getAdapter(platform);
const thread = ThreadImpl.fromJSON(serializedThread, adapter);

let sent: { id: string; threadId: string };
let postPromise: Promise<{ id: string; threadId: string }>;
if (content.card) {
sent = await thread.post(content.card);
postPromise = thread.post(content.card);
} else if (content.markdown !== undefined) {
sent = await thread.post({ markdown: content.markdown, files: content.files });
postPromise = thread.post({ markdown: content.markdown, files: content.files });
} else {
sent = await thread.post(content.text ?? '');
postPromise = thread.post(content.text ?? '');
}

const sent = await postPromise.catch(toDeliveryError);

return { messageId: sent.id, platformThreadId: sent.threadId };
}

Expand All @@ -163,22 +175,24 @@ export class ChatSdkService implements OnModuleDestroy {
throw new BadRequestException(`Platform ${platform} does not support editing messages`);
}

let edited: { id: string; threadId: string };
let editPromise: Promise<{ id: string; threadId: string }>;
if (content.card) {
edited = await adapter.editMessage(
editPromise = adapter.editMessage(
platformThreadId,
platformMessageId,
content.card as unknown as AdapterPostableMessage
);
} else if (content.markdown !== undefined) {
edited = await adapter.editMessage(platformThreadId, platformMessageId, {
editPromise = adapter.editMessage(platformThreadId, platformMessageId, {
markdown: content.markdown,
files: content.files,
} as unknown as AdapterPostableMessage);
} else {
edited = await adapter.editMessage(platformThreadId, platformMessageId, content.text ?? '');
editPromise = adapter.editMessage(platformThreadId, platformMessageId, content.text ?? '');
}

const edited = await editPromise.catch(toDeliveryError);

return { messageId: edited.id, platformThreadId: edited.threadId };
}

Expand Down Expand Up @@ -419,7 +433,7 @@ export class ChatSdkService implements OnModuleDestroy {
},
};

const result = await handler.send(mailOptions);
const result = await handler.send(mailOptions).catch(toDeliveryError);

return { messageId: result?.id || params.messageId || '' };
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { BadGatewayException, BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { decryptCredentials, InstrumentUsecase, MailFactory } from '@novu/application-generic';
import { AgentIntegrationRepository, AgentRepository, IntegrationRepository } from '@novu/dal';
import { ChannelTypeEnum, EmailProviderIdEnum, IEmailOptions } from '@novu/shared';
Expand Down Expand Up @@ -85,7 +85,15 @@ export class SendAgentTestEmail {
senderName: (senderIntegration.credentials?.senderName as string) || 'Novu',
};

await handler.send(mailOptions);
await handler.send(mailOptions).catch((err) => {
const base = err instanceof Error ? err.message : String(err);
const body = (err as any)?.response?.body;
const detail = Array.isArray(body?.errors) ? body.errors[0]?.message : body?.message;
throw new BadGatewayException({
error: 'delivery_failed',
message: detail ? `${base}: ${detail}` : base,
});
});
Comment thread
ChmaraX marked this conversation as resolved.

return { success: true };
}
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
} from './resources';
export {
Actions,
AgentDeliveryError,
AgentEventEnum,
agent,
Button,
Expand Down
13 changes: 13 additions & 0 deletions packages/framework/src/resources/agent/agent.context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isJSX, toCardElement } from 'chat/jsx-runtime';
import { AgentDeliveryError } from './agent.errors';
import type {
AgentAction,
AgentBridgeRequest,
Expand Down Expand Up @@ -216,6 +217,18 @@ export class AgentContextImpl implements AgentContext {

if (!response.ok) {
const text = await response.text().catch(() => '');

if (response.status === 502) {
let message = text;
try {
const parsed = JSON.parse(text) as { message?: string };
if (parsed.message) message = parsed.message;
} catch {
// use raw text if JSON parsing fails
}
throw new AgentDeliveryError(502, message);
}

throw new Error(`Agent reply failed (${response.status}): ${text}`);
}

Expand Down
32 changes: 32 additions & 0 deletions packages/framework/src/resources/agent/agent.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Thrown by `ctx.reply()` and `handle.edit()` when the upstream message delivery
* fails — e.g. the configured email provider returns 401, Slack rejects the token,
* or Teams rejects the request.
*
* The `message` property contains the original provider error text
*
* @example
* ```ts
* import { AgentDeliveryError } from '@novu/framework';
*
* try {
* await ctx.reply('Hello!');
* } catch (err) {
* if (err instanceof AgentDeliveryError) {
* // Delivery failed (misconfigured provider, rate limit, etc.)
* console.error('Delivery failed:', err.message);
* return;
* }
* throw err;
* }
* ```
*/
export class AgentDeliveryError extends Error {
readonly statusCode: number;

constructor(statusCode: number, message: string) {
super(message);
this.name = 'AgentDeliveryError';
this.statusCode = statusCode;
}
}
Comment on lines +24 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Check the framework's compile target(s) — ES2015+ means instanceof works natively.
fd -t f -i 'tsconfig*.json' packages/framework -x sh -c 'echo "=== {} ==="; cat {}'

# 2) Confirm version of `@novu/framework` (PR should bump it because of the new public export).
fd -t f '^package.json$' packages/framework --max-depth 2 -x sh -c 'echo "=== {} ==="; jq "{name,version}" {}'

# 3) Confirm AgentDeliveryError is not silently re-introducing a name clash anywhere else.
rg -nP '\bclass\s+AgentDeliveryError\b'

Repository: novuhq/novu

Length of output: 1977


🏁 Script executed:

#!/bin/bash

# 1) Check if AgentDeliveryError is exported from the main `@novu/framework` entry point
echo "=== Checking main entry points for `@novu/framework` ==="
fd -t f '^index\.(ts|js)$' packages/framework/src --max-depth 1 -x sh -c 'echo "=== {} ==="; head -50 {}'

# 2) Search for AgentDeliveryError exports across the codebase
echo ""
echo "=== Searching for AgentDeliveryError exports ==="
rg -n 'export.*AgentDeliveryError|from.*agent\.errors'

# 3) Check if the version in package.json was changed in recent commits
echo ""
echo "=== Checking git history for version changes ==="
git log --oneline -20 -- packages/framework/package.json

# 4) Get the previous version to confirm if a bump is needed
echo ""
echo "=== Previous package versions ==="
git show HEAD~1:packages/framework/package.json 2>/dev/null | jq '.version' || echo "Could not retrieve previous version"

Repository: novuhq/novu

Length of output: 1602


🏁 Script executed:

#!/bin/bash

# Check current git status and compare versions
echo "=== Current package.json version ==="
cat packages/framework/package.json | jq '.version'

echo ""
echo "=== Check if there are uncommitted changes to package.json ==="
git status --porcelain packages/framework/package.json

echo ""
echo "=== List recent commits to see if version was already bumped ==="
git log --oneline -30 | head -15

echo ""
echo "=== Check the actual diff if package.json was modified ==="
git diff HEAD -- packages/framework/package.json | head -20

Repository: novuhq/novu

Length of output: 388


🏁 Script executed:

#!/bin/bash

# Double-check if AgentDeliveryError was actually new in this commit
echo "=== Search for AgentDeliveryError in git history ==="
git log --all --oneline --grep="AgentDeliveryError" | head -10

echo ""
echo "=== Check if agent.errors.ts exists in parent commit ==="
git show HEAD~1:packages/framework/src/resources/agent/agent.errors.ts 2>/dev/null | head -30 || echo "File did not exist in parent commit"

echo ""
echo "=== Verify this is truly a new export by checking the agent resource index ==="
git show HEAD~1:packages/framework/src/resources/agent/index.ts 2>/dev/null | grep -i "AgentDeliveryError" || echo "AgentDeliveryError not exported in parent commit"

Repository: novuhq/novu

Length of output: 384


Bump @novu/framework version for new public export AgentDeliveryError.

AgentDeliveryError is a new public export from @novu/framework (added in this commit, not present in parent), so the package version must be incremented at minimum as a minor bump per semver. Current version remains 2.10.1-alpha.2 with no changes to package.json.

Update packages/framework/package.json to reflect the new export (e.g., 2.11.0-alpha.0 or equivalent per your versioning scheme).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.errors.ts` around lines 24 - 32,
The package version for `@novu/framework` must be bumped to publish the new public
export AgentDeliveryError; open the framework package.json and update the
version field (e.g., from 2.10.1-alpha.2 to 2.11.0-alpha.0 or your project's
equivalent minor-alpha scheme), run any required lockfile install/update, and
ensure the new version is committed so the AgentDeliveryError export is included
in the published package.

1 change: 1 addition & 0 deletions packages/framework/src/resources/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
} from 'chat';
export { Actions, Button, Card, CardLink, CardText, Divider, Select, SelectOption, TextInput } from 'chat';
export { AgentContextImpl } from './agent.context';
export { AgentDeliveryError } from './agent.errors';
export { agent } from './agent.resource';
export type {
Agent,
Expand Down
Loading