From 08e76edd517a9ebc36a93d6a35b9df3bf99f91af Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:54:11 +0530 Subject: [PATCH 01/15] remove user-facing errors --- .../app/api/copilot-stream-response/[streamId]/route.ts | 2 +- apps/rowboat/app/api/stream-response/[streamId]/route.ts | 2 +- apps/rowboat/app/api/v1/[projectId]/chat/route.ts | 2 +- apps/rowboat/app/scripts/rag-worker.ts | 4 ++-- .../src/application/lib/agents-runtime/agent-tools.ts | 8 ++++---- apps/rowboat/src/application/workers/jobs.worker.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/rowboat/app/api/copilot-stream-response/[streamId]/route.ts b/apps/rowboat/app/api/copilot-stream-response/[streamId]/route.ts index 3b743a3a..1e540933 100644 --- a/apps/rowboat/app/api/copilot-stream-response/[streamId]/route.ts +++ b/apps/rowboat/app/api/copilot-stream-response/[streamId]/route.ts @@ -53,7 +53,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: } } catch (error) { console.error('Error processing copilot stream:', error); - controller.error(error); + controller.error(new Error("Something went wrong. Please try again.")); } finally { // log copilot usage if (USE_BILLING && billingCustomerId) { diff --git a/apps/rowboat/app/api/stream-response/[streamId]/route.ts b/apps/rowboat/app/api/stream-response/[streamId]/route.ts index 4fc9de3f..17c1a8d8 100644 --- a/apps/rowboat/app/api/stream-response/[streamId]/route.ts +++ b/apps/rowboat/app/api/stream-response/[streamId]/route.ts @@ -31,7 +31,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: console.error('Error processing stream:', error); const errMessage: z.infer = { type: "error", - error: `Error processing stream: ${error}`, + error: "Something went wrong. Please try again.", isBillingError: false, }; controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(errMessage)}\n\n`)); diff --git a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts index 73a48348..95234692 100644 --- a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts +++ b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts @@ -55,7 +55,7 @@ export async function POST( controller.close(); } catch (error) { logger.log(`Error processing stream: ${error}`); - controller.error(error); + controller.error(new Error("Something went wrong. Please try again.")); } }, }); diff --git a/apps/rowboat/app/scripts/rag-worker.ts b/apps/rowboat/app/scripts/rag-worker.ts index a0020b9c..3142a652 100644 --- a/apps/rowboat/app/scripts/rag-worker.ts +++ b/apps/rowboat/app/scripts/rag-worker.ts @@ -433,7 +433,7 @@ async function runDeletionPipeline(_logger: PrefixLogger, job: z.infer Date: Wed, 20 Aug 2025 16:40:20 +0530 Subject: [PATCH 02/15] improve minute alignment logic --- .../application/lib/utils/time-to-next-minute.ts | 6 ++++++ .../src/application/workers/job-rules.worker.ts | 15 +++------------ .../policies/redis.usage-quota.policy.ts | 3 ++- 3 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 apps/rowboat/src/application/lib/utils/time-to-next-minute.ts diff --git a/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts b/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts new file mode 100644 index 00000000..b5996681 --- /dev/null +++ b/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts @@ -0,0 +1,6 @@ +// returns the number of seconds until the next minute +export function secondsToNextMinute(): number { + const now = new Date(); + const secondsUntilNextMinute = 60 - now.getSeconds(); + return secondsUntilNextMinute; +} \ No newline at end of file diff --git a/apps/rowboat/src/application/workers/job-rules.worker.ts b/apps/rowboat/src/application/workers/job-rules.worker.ts index 5b03e025..039665b7 100644 --- a/apps/rowboat/src/application/workers/job-rules.worker.ts +++ b/apps/rowboat/src/application/workers/job-rules.worker.ts @@ -8,6 +8,7 @@ import { z } from "zod"; import { nanoid } from "nanoid"; import { PrefixLogger } from "@/app/lib/utils"; import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule"; +import { secondsToNextMinute } from "../lib/utils/time-to-next-minute"; export interface IJobRulesWorker { run(): Promise; @@ -20,8 +21,6 @@ export class JobRulesWorker implements IJobRulesWorker { private readonly jobsRepository: IJobsRepository; private readonly projectsRepository: IProjectsRepository; private readonly pubSubService: IPubSubService; - // Run polls aligned to minute marks at this offset (e.g., 2000 ms => :02 each minute) - private readonly minuteAlignmentOffsetMs: number = 2_000; private workerId: string; private logger: PrefixLogger; private isRunning: boolean = false; @@ -131,14 +130,6 @@ export class JobRulesWorker implements IJobRulesWorker { } } - // Calculates delay so the next run happens at next minute + minuteAlignmentOffsetMs - private calculateDelayToNextAlignedMinute(): number { - const now = new Date(); - const millisecondsUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds(); - const delayMs = millisecondsUntilNextMinute + this.minuteAlignmentOffsetMs; - return delayMs > 0 ? delayMs : this.minuteAlignmentOffsetMs; - } - private async pollScheduled(): Promise { const logger = this.logger.child(`poll-scheduled`); logger.log("Polling..."); @@ -176,7 +167,8 @@ export class JobRulesWorker implements IJobRulesWorker { } private scheduleNextPoll(): void { - const delayMs = this.calculateDelayToNextAlignedMinute(); + // schedule next poll for 2 s past the minute mark + const delayMs = (secondsToNextMinute() + 2) * 1000; this.logger.log(`Scheduling next poll in ${delayMs} ms`); this.pollTimeoutId = setTimeout(async () => { if (!this.isRunning) return; @@ -195,7 +187,6 @@ export class JobRulesWorker implements IJobRulesWorker { } this.isRunning = true; this.logger.log(`Starting worker ${this.workerId}`); - // No immediate polling; align to 2s past the next minute this.scheduleNextPoll(); } diff --git a/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts b/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts index 05fecbdb..29fe2d08 100644 --- a/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts +++ b/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts @@ -1,6 +1,7 @@ import { IUsageQuotaPolicy } from "@/src/application/policies/usage-quota.policy.interface"; import { redisClient } from "@/app/lib/redis"; import { QuotaExceededError } from "@/src/entities/errors/common"; +import { secondsToNextMinute } from "@/src/application/lib/utils/time-to-next-minute"; const MAX_QUERIES_PER_MINUTE = Number(process.env.MAX_QUERIES_PER_MINUTE) || 0; @@ -15,7 +16,7 @@ export class RedisUsageQuotaPolicy implements IUsageQuotaPolicy { const count = await redisClient.incr(key); if (count === 1) { - await redisClient.expire(key, 70); // Set TTL to clean up automatically + await redisClient.expire(key, secondsToNextMinute()); // Set TTL to clean up automatically } if (count > MAX_QUERIES_PER_MINUTE) { From d2e590956bd6c9344f14f82df68a2baaf7bfda1b Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:08:19 +0530 Subject: [PATCH 03/15] enforce max jobs per hour --- apps/rowboat/app/actions/copilot.actions.ts | 6 ++--- .../lib/utils/time-to-next-minute.ts | 6 +++++ .../policies/usage-quota.policy.interface.ts | 21 +++++++++++++-- ...te-composio-trigger-deployment.use-case.ts | 2 +- ...te-composio-trigger-deployment.use-case.ts | 2 +- ...ch-composio-trigger-deployment.use-case.ts | 2 +- ...t-composio-trigger-deployments.use-case.ts | 2 +- .../create-cached-turn.use-case.ts | 2 +- .../create-conversation.use-case.ts | 2 +- .../fetch-cached-turn.use-case.ts | 2 +- .../fetch-conversation.use-case.ts | 2 +- .../list-conversations.use-case.ts | 2 +- .../run-conversation-turn.use-case.ts | 2 +- .../add-docs-to-data-source.use-case.ts | 2 +- .../create-data-source.use-case.ts | 2 +- .../delete-data-source.use-case.ts | 2 +- .../delete-doc-from-data-source.use-case.ts | 2 +- .../fetch-data-source.use-case.ts | 2 +- .../get-download-url-for-file.use-case.ts | 2 +- .../get-upload-urls-for-files.use-case.ts | 2 +- .../list-data-sources.use-case.ts | 2 +- .../list-docs-in-data-source.use-case.ts | 2 +- .../recrawl-web-data-source.use-case.ts | 2 +- .../toggle-data-source.use-case.ts | 2 +- .../update-data-source.use-case.ts | 2 +- .../use-cases/jobs/fetch-job.use-case.ts | 2 +- .../use-cases/jobs/list-jobs.use-case.ts | 2 +- .../add-custom-mcp-server.use-case.ts | 2 +- ...osio-managed-connected-account.use-case.ts | 2 +- ...reate-custom-connected-account.use-case.ts | 2 +- .../projects/create-project.use-case.ts | 2 +- ...ete-composio-connected-account.use-case.ts | 2 +- .../projects/fetch-project.use-case.ts | 2 +- .../projects/get-composio-toolkit.use-case.ts | 2 +- .../list-composio-toolkits.use-case.ts | 2 +- .../projects/list-composio-tools.use-case.ts | 2 +- .../remove-custom-mcp-server.use-case.ts | 2 +- .../revert-to-live-workflow.use-case.ts | 2 +- .../projects/rotate-secret.use-case.ts | 2 +- .../sync-connected-account.use-case.ts | 2 +- .../update-draft-workflow.use-case.ts | 2 +- .../projects/update-live-workflow.use-case.ts | 2 +- .../projects/update-project-name.use-case.ts | 2 +- .../projects/update-webhook-url.use-case.ts | 2 +- .../create-recurring-job-rule.use-case.ts | 2 +- .../delete-recurring-job-rule.use-case.ts | 2 +- .../fetch-recurring-job-rule.use-case.ts | 2 +- .../list-recurring-job-rules.use-case.ts | 2 +- .../toggle-recurring-job-rule.use-case.ts | 2 +- .../create-scheduled-job-rule.use-case.ts | 2 +- .../delete-scheduled-job-rule.use-case.ts | 2 +- .../fetch-scheduled-job-rule.use-case.ts | 2 +- .../list-scheduled-job-rules.use-case.ts | 2 +- .../src/application/workers/jobs.worker.ts | 27 ++++++++++++++++--- .../policies/redis.usage-quota.policy.ts | 23 ++++++++++++++-- 55 files changed, 123 insertions(+), 60 deletions(-) diff --git a/apps/rowboat/app/actions/copilot.actions.ts b/apps/rowboat/app/actions/copilot.actions.ts index 5dc6c897..5712e0af 100644 --- a/apps/rowboat/app/actions/copilot.actions.ts +++ b/apps/rowboat/app/actions/copilot.actions.ts @@ -28,7 +28,7 @@ export async function getCopilotResponseStream( streamId: string; } | { billingError: string }> { await projectAuthCheck(projectId); - await usageQuotaPolicy.assertAndConsume(projectId); + await usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // Check billing authorization const authResponse = await authorizeUserAction({ @@ -38,7 +38,7 @@ export async function getCopilotResponseStream( return { billingError: authResponse.error || 'Billing error' }; } - await usageQuotaPolicy.assertAndConsume(projectId); + await usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // prepare request const request: z.infer = { @@ -70,7 +70,7 @@ export async function getCopilotAgentInstructions( agentName: string, ): Promise { await projectAuthCheck(projectId); - await usageQuotaPolicy.assertAndConsume(projectId); + await usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // Check billing authorization const authResponse = await authorizeUserAction({ diff --git a/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts b/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts index b5996681..808cbe99 100644 --- a/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts +++ b/apps/rowboat/src/application/lib/utils/time-to-next-minute.ts @@ -3,4 +3,10 @@ export function secondsToNextMinute(): number { const now = new Date(); const secondsUntilNextMinute = 60 - now.getSeconds(); return secondsUntilNextMinute; +} + +export function minutesToNextHour(): number { + const now = new Date(); + const minutesUntilNextHour = 60 - now.getMinutes(); + return minutesUntilNextHour; } \ No newline at end of file diff --git a/apps/rowboat/src/application/policies/usage-quota.policy.interface.ts b/apps/rowboat/src/application/policies/usage-quota.policy.interface.ts index 20618588..4e311d63 100644 --- a/apps/rowboat/src/application/policies/usage-quota.policy.interface.ts +++ b/apps/rowboat/src/application/policies/usage-quota.policy.interface.ts @@ -1,4 +1,21 @@ +import { QuotaExceededError } from "@/src/entities/errors/common"; + export interface IUsageQuotaPolicy { - // this method will throw a QuotaExceededError if the quota is exceeded - assertAndConsume(projectId: string): Promise; + /** + * Asserts that the project has not exceeded its usage quota and consumes the action. + * Used for general project actions. + * + * @param projectId - The ID of the project to assert and consume. + * @throws QuotaExceededError if the quota is exceeded. + */ + assertAndConsumeProjectAction(projectId: string): Promise; + + + /** + * Asserts that the project has not exceeded its usage quota for running jobs. + * + * @param projectId - The ID of the project to assert and consume. + * @throws QuotaExceededError if the quota is exceeded. + */ + assertAndConsumeRunJobAction(projectId: string): Promise; } \ No newline at end of file diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts index fdd3bbfc..1b6c3786 100644 --- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts @@ -59,7 +59,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // get trigger type info const triggerType = await getTriggersType(request.data.triggerTypeSlug); diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case.ts index f3d998f2..26373815 100644 --- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case.ts @@ -54,7 +54,7 @@ export class DeleteComposioTriggerDeploymentUseCase implements IDeleteComposioTr }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // ensure deployment belongs to this project const deployment = await this.composioTriggerDeploymentsRepository.fetch(request.deploymentId); diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts index ed0be2de..32aa7909 100644 --- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts @@ -53,7 +53,7 @@ export class FetchComposioTriggerDeploymentUseCase implements IFetchComposioTrig }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return deployment; } diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case.ts index d233b096..f915e187 100644 --- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case.ts @@ -51,7 +51,7 @@ export class ListComposioTriggerDeploymentsUseCase implements IListComposioTrigg }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch deployments for project return await this.composioTriggerDeploymentsRepository.listByProjectId(projectId, request.cursor, limit); diff --git a/apps/rowboat/src/application/use-cases/conversations/create-cached-turn.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/create-cached-turn.use-case.ts index 3d7bdb3b..c122e7f4 100644 --- a/apps/rowboat/src/application/use-cases/conversations/create-cached-turn.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/create-cached-turn.use-case.ts @@ -61,7 +61,7 @@ export class CreateCachedTurnUseCase implements ICreateCachedTurnUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // create cache entry const key = nanoid(); diff --git a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts index e3ac496a..d73ac340 100644 --- a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts @@ -61,7 +61,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase { } // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // if workflow is not provided, fetch workflow if (!workflow) { diff --git a/apps/rowboat/src/application/use-cases/conversations/fetch-cached-turn.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/fetch-cached-turn.use-case.ts index 273fdab3..3154f6f7 100644 --- a/apps/rowboat/src/application/use-cases/conversations/fetch-cached-turn.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/fetch-cached-turn.use-case.ts @@ -68,7 +68,7 @@ export class FetchCachedTurnUseCase implements IFetchCachedTurnUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // delete from cache await this.cacheService.delete(`turn-${data.key}`); diff --git a/apps/rowboat/src/application/use-cases/conversations/fetch-conversation.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/fetch-conversation.use-case.ts index b6a5ed26..0e1f5c01 100644 --- a/apps/rowboat/src/application/use-cases/conversations/fetch-conversation.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/fetch-conversation.use-case.ts @@ -54,7 +54,7 @@ export class FetchConversationUseCase implements IFetchConversationUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // return the conversation return conversation; diff --git a/apps/rowboat/src/application/use-cases/conversations/list-conversations.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/list-conversations.use-case.ts index d3b69afa..7891cc71 100644 --- a/apps/rowboat/src/application/use-cases/conversations/list-conversations.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/list-conversations.use-case.ts @@ -51,7 +51,7 @@ export class ListConversationsUseCase implements IListConversationsUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch conversations for project return await this.conversationsRepository.list(projectId, request.cursor, limit); diff --git a/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts index d8b05aed..635a960d 100644 --- a/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts @@ -62,7 +62,7 @@ export class RunConversationTurnUseCase implements IRunConversationTurnUseCase { } // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // Check billing auth let billingCustomerId: string | null = null; diff --git a/apps/rowboat/src/application/use-cases/data-sources/add-docs-to-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/add-docs-to-data-source.use-case.ts index be0cf3f3..2d732788 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/add-docs-to-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/add-docs-to-data-source.use-case.ts @@ -55,7 +55,7 @@ export class AddDocsToDataSourceUseCase implements IAddDocsToDataSourceUseCase { projectId: source.projectId, }); - await this.usageQuotaPolicy.assertAndConsume(source.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId); await this.dataSourceDocsRepository.bulkCreate(source.projectId, sourceId, docs); diff --git a/apps/rowboat/src/application/use-cases/data-sources/create-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/create-data-source.use-case.ts index 3c7c4dbb..7d5e7110 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/create-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/create-data-source.use-case.ts @@ -44,7 +44,7 @@ export class CreateDataSourceUseCase implements ICreateDataSourceUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); let _status = "pending"; // Only set status for non-file data sources diff --git a/apps/rowboat/src/application/use-cases/data-sources/delete-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/delete-data-source.use-case.ts index b09c38b0..9ef47942 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/delete-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/delete-data-source.use-case.ts @@ -49,7 +49,7 @@ export class DeleteDataSourceUseCase implements IDeleteDataSourceUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); await this.dataSourcesRepository.update(request.sourceId, { status: 'deleted', diff --git a/apps/rowboat/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case.ts index f879f62d..7490a702 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/delete-doc-from-data-source.use-case.ts @@ -54,7 +54,7 @@ export class DeleteDocFromDataSourceUseCase implements IDeleteDocFromDataSourceU projectId: doc.projectId, }); - await this.usageQuotaPolicy.assertAndConsume(doc.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(doc.projectId); await this.dataSourceDocsRepository.markAsDeleted(docId); diff --git a/apps/rowboat/src/application/use-cases/data-sources/fetch-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/fetch-data-source.use-case.ts index f3d5f0a2..19b6a311 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/fetch-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/fetch-data-source.use-case.ts @@ -50,7 +50,7 @@ export class FetchDataSourceUseCase implements IFetchDataSourceUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return source; } diff --git a/apps/rowboat/src/application/use-cases/data-sources/get-download-url-for-file.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/get-download-url-for-file.use-case.ts index f3da5d6e..e45dabfe 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/get-download-url-for-file.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/get-download-url-for-file.use-case.ts @@ -58,7 +58,7 @@ export class GetDownloadUrlForFileUseCase implements IGetDownloadUrlForFileUseCa projectId: file.projectId, }); - await this.usageQuotaPolicy.assertAndConsume(file.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(file.projectId); if (file.data.type === 'file_local') { // use the file id instead of path here diff --git a/apps/rowboat/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case.ts index d044d85a..348fa7bd 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/get-upload-urls-for-files.use-case.ts @@ -60,7 +60,7 @@ export class GetUploadUrlsForFilesUseCase implements IGetUploadUrlsForFilesUseCa projectId: source.projectId, }); - await this.usageQuotaPolicy.assertAndConsume(source.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId); const urls: { fileId: string, uploadUrl: string, path: string }[] = []; for (const file of files) { diff --git a/apps/rowboat/src/application/use-cases/data-sources/list-data-sources.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/list-data-sources.use-case.ts index 851266a0..99f70eae 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/list-data-sources.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/list-data-sources.use-case.ts @@ -44,7 +44,7 @@ export class ListDataSourcesUseCase implements IListDataSourcesUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // list all sources for now const sources = []; diff --git a/apps/rowboat/src/application/use-cases/data-sources/list-docs-in-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/list-docs-in-data-source.use-case.ts index 7a1eb9e4..94c66877 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/list-docs-in-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/list-docs-in-data-source.use-case.ts @@ -55,7 +55,7 @@ export class ListDocsInDataSourceUseCase implements IListDocsInDataSourceUseCase projectId: source.projectId, }); - await this.usageQuotaPolicy.assertAndConsume(source.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId); // fetch all docs const docs = []; diff --git a/apps/rowboat/src/application/use-cases/data-sources/recrawl-web-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/recrawl-web-data-source.use-case.ts index 6ebf11dc..d9572034 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/recrawl-web-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/recrawl-web-data-source.use-case.ts @@ -58,7 +58,7 @@ export class RecrawlWebDataSourceUseCase implements IRecrawlWebDataSourceUseCase projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); await this.dataSourceDocsRepository.markSourceDocsPending(request.sourceId); diff --git a/apps/rowboat/src/application/use-cases/data-sources/toggle-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/toggle-data-source.use-case.ts index 47f141a2..cafc0fbc 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/toggle-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/toggle-data-source.use-case.ts @@ -51,7 +51,7 @@ export class ToggleDataSourceUseCase implements IToggleDataSourceUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await this.dataSourcesRepository.update(request.sourceId, { active: request.active }); } diff --git a/apps/rowboat/src/application/use-cases/data-sources/update-data-source.use-case.ts b/apps/rowboat/src/application/use-cases/data-sources/update-data-source.use-case.ts index abed87fd..fa4c08e2 100644 --- a/apps/rowboat/src/application/use-cases/data-sources/update-data-source.use-case.ts +++ b/apps/rowboat/src/application/use-cases/data-sources/update-data-source.use-case.ts @@ -55,7 +55,7 @@ export class UpdateDataSourceUseCase implements IUpdateDataSourceUseCase { projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await this.dataSourcesRepository.update(request.sourceId, request.data, true); } diff --git a/apps/rowboat/src/application/use-cases/jobs/fetch-job.use-case.ts b/apps/rowboat/src/application/use-cases/jobs/fetch-job.use-case.ts index 0156bc64..e18627fe 100644 --- a/apps/rowboat/src/application/use-cases/jobs/fetch-job.use-case.ts +++ b/apps/rowboat/src/application/use-cases/jobs/fetch-job.use-case.ts @@ -54,7 +54,7 @@ export class FetchJobUseCase implements IFetchJobUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // return the job return job; diff --git a/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts b/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts index 95d4474a..d91adeae 100644 --- a/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts +++ b/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts @@ -52,7 +52,7 @@ export class ListJobsUseCase implements IListJobsUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch jobs for project return await this.jobsRepository.list(projectId, request.filters, request.cursor, limit); diff --git a/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts b/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts index dd4669ac..fc5855c3 100644 --- a/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts @@ -48,7 +48,7 @@ export class AddCustomMcpServerUseCase implements IAddCustomMcpServerUseCase { const { caller, userId, apiKey, projectId, name } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // Validate server URL const serverUrl = validateHttpHttpsUrl(request.server.serverUrl); diff --git a/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts index 4536c8fd..c53b7aa9 100644 --- a/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts @@ -44,7 +44,7 @@ export class CreateComposioManagedConnectedAccountUseCase implements ICreateComp const { caller, userId, apiKey, projectId, toolkitSlug, callbackUrl } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch managed auth configs const configs = await listAuthConfigs(toolkitSlug, null, true); diff --git a/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts index da3110fa..c547a1e9 100644 --- a/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts @@ -50,7 +50,7 @@ export class CreateCustomConnectedAccountUseCase implements ICreateCustomConnect const { caller, userId, apiKey, projectId, toolkitSlug, authConfig, callbackUrl } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // create custom auth config const created: z.infer = await createAuthConfig({ diff --git a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts index 260139cd..c439c788 100644 --- a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts @@ -113,7 +113,7 @@ export class CreateProjectUseCase implements ICreateProjectUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(project.id); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(project.id); return project; } diff --git a/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts index cdc36f22..56cce560 100644 --- a/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts @@ -56,7 +56,7 @@ export class DeleteComposioConnectedAccountUseCase implements IDeleteComposioCon }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch project const project = await this.projectsRepository.fetch(projectId); diff --git a/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts index 7773c38f..03b292c3 100644 --- a/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts @@ -52,7 +52,7 @@ export class FetchProjectUseCase implements IFetchProjectUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await this.projectsRepository.fetch(projectId); } diff --git a/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts b/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts index b7611069..a2d2d9a6 100644 --- a/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts @@ -28,7 +28,7 @@ export class GetComposioToolkitUseCase implements IGetComposioToolkitUseCase { async execute(request: z.infer): Promise> { const { caller, userId, apiKey, projectId, toolkitSlug } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await getToolkit(toolkitSlug); } } diff --git a/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts b/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts index becf43d7..b952f342 100644 --- a/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts @@ -29,7 +29,7 @@ export class ListComposioToolkitsUseCase implements IListComposioToolkitsUseCase async execute(request: z.infer): Promise>>> { const { caller, userId, apiKey, projectId, cursor } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await listToolkits(cursor ?? null); } } diff --git a/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts b/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts index 3a53992c..04278aa9 100644 --- a/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts @@ -31,7 +31,7 @@ export class ListComposioToolsUseCase implements IListComposioToolsUseCase { async execute(request: z.infer): Promise>>> { const { caller, userId, apiKey, projectId, toolkitSlug, searchQuery, cursor } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); return await listTools(toolkitSlug, searchQuery ?? null, cursor ?? null); } } diff --git a/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts b/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts index 6a588045..74455fbb 100644 --- a/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts @@ -37,7 +37,7 @@ export class RemoveCustomMcpServerUseCase implements IRemoveCustomMcpServerUseCa async execute(request: z.infer): Promise { const { caller, userId, apiKey, projectId, name } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); await this.projectsRepository.deleteCustomMcpServer(projectId, name); } } diff --git a/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts index 150dc5f8..a8d0aae4 100644 --- a/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts @@ -42,7 +42,7 @@ export class RevertToLiveWorkflowUseCase implements IRevertToLiveWorkflowUseCase apiKey: request.apiKey, projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); const project = await this.projectsRepository.fetch(projectId); if (!project) { diff --git a/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts b/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts index f2abdbaa..67e33737 100644 --- a/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts @@ -37,7 +37,7 @@ export class RotateSecretUseCase implements IRotateSecretUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); const secret = crypto.randomBytes(32).toString("hex"); await this.projectsRepository.updateSecret(projectId, secret); diff --git a/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts index c3d908fc..089585c9 100644 --- a/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts @@ -42,7 +42,7 @@ export class SyncConnectedAccountUseCase implements ISyncConnectedAccountUseCase const { caller, userId, apiKey, projectId, toolkitSlug, connectedAccountId } = request; await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch project & account to verify const project = await this.projectsRepository.fetch(projectId); diff --git a/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts index fe1ecc37..14743e61 100644 --- a/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts @@ -43,7 +43,7 @@ export class UpdateDraftWorkflowUseCase implements IUpdateDraftWorkflowUseCase { apiKey: request.apiKey, projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer; await this.projectsRepository.updateDraftWorkflow(projectId, workflow); diff --git a/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts index e1c770f0..ebf13e38 100644 --- a/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts @@ -43,7 +43,7 @@ export class UpdateLiveWorkflowUseCase implements IUpdateLiveWorkflowUseCase { apiKey: request.apiKey, projectId, }); - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer; await this.projectsRepository.updateLiveWorkflow(projectId, workflow); diff --git a/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts index 7c768fc3..b1562d1d 100644 --- a/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts @@ -36,7 +36,7 @@ export class UpdateProjectNameUseCase implements IUpdateProjectNameUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); await this.projectsRepository.updateName(projectId, name); } diff --git a/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts index 56ed6268..b71fd487 100644 --- a/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts @@ -36,7 +36,7 @@ export class UpdateWebhookUrlUseCase implements IUpdateWebhookUrlUseCase { }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); await this.projectsRepository.updateWebhookUrl(projectId, url); } diff --git a/apps/rowboat/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case.ts index e7bf8365..0c2c2c0c 100644 --- a/apps/rowboat/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/recurring-job-rules/create-recurring-job-rule.use-case.ts @@ -55,7 +55,7 @@ export class CreateRecurringJobRuleUseCase implements ICreateRecurringJobRuleUse }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(request.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId); // create the recurring job rule const rule = await this.recurringJobRulesRepository.create({ diff --git a/apps/rowboat/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case.ts index dec59bca..2cb3c98e 100644 --- a/apps/rowboat/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case.ts @@ -45,7 +45,7 @@ export class DeleteRecurringJobRuleUseCase implements IDeleteRecurringJobRuleUse }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(request.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId); // ensure rule belongs to this project const rule = await this.recurringJobRulesRepository.fetch(request.ruleId); diff --git a/apps/rowboat/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case.ts index cd89ba2c..6d3846d9 100644 --- a/apps/rowboat/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/recurring-job-rules/fetch-recurring-job-rule.use-case.ts @@ -54,7 +54,7 @@ export class FetchRecurringJobRuleUseCase implements IFetchRecurringJobRuleUseCa }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // return the rule return rule; diff --git a/apps/rowboat/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case.ts b/apps/rowboat/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case.ts index 22f45186..9dbe0a42 100644 --- a/apps/rowboat/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case.ts +++ b/apps/rowboat/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case.ts @@ -49,7 +49,7 @@ export class ListRecurringJobRulesUseCase implements IListRecurringJobRulesUseCa }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch recurring job rules for project return await this.recurringJobRulesRepository.list(projectId, request.cursor, limit); diff --git a/apps/rowboat/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case.ts index e7258fa5..8bb52a28 100644 --- a/apps/rowboat/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case.ts @@ -55,7 +55,7 @@ export class ToggleRecurringJobRuleUseCase implements IToggleRecurringJobRuleUse }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // update the rule return await this.recurringJobRulesRepository.toggle(request.ruleId, request.disabled); diff --git a/apps/rowboat/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case.ts index 06170434..544a5519 100644 --- a/apps/rowboat/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case.ts @@ -50,7 +50,7 @@ export class CreateScheduledJobRuleUseCase implements ICreateScheduledJobRuleUse }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(request.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId); // create the scheduled job rule with UTC time const rule = await this.scheduledJobRulesRepository.create({ diff --git a/apps/rowboat/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case.ts index b7b7123f..6977fbbd 100644 --- a/apps/rowboat/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case.ts @@ -45,7 +45,7 @@ export class DeleteScheduledJobRuleUseCase implements IDeleteScheduledJobRuleUse }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(request.projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId); // ensure rule belongs to this project const rule = await this.scheduledJobRulesRepository.fetch(request.ruleId); diff --git a/apps/rowboat/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case.ts b/apps/rowboat/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case.ts index 6b5004cd..33b6b78c 100644 --- a/apps/rowboat/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case.ts +++ b/apps/rowboat/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case.ts @@ -54,7 +54,7 @@ export class FetchScheduledJobRuleUseCase implements IFetchScheduledJobRuleUseCa }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // return the scheduled job rule return rule; diff --git a/apps/rowboat/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case.ts b/apps/rowboat/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case.ts index b5f79ce8..17a929fc 100644 --- a/apps/rowboat/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case.ts +++ b/apps/rowboat/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case.ts @@ -49,7 +49,7 @@ export class ListScheduledJobRulesUseCase implements IListScheduledJobRulesUseCa }); // assert and consume quota - await this.usageQuotaPolicy.assertAndConsume(projectId); + await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId); // fetch scheduled job rules for project return await this.scheduledJobRulesRepository.list(projectId, request.cursor, limit); diff --git a/apps/rowboat/src/application/workers/jobs.worker.ts b/apps/rowboat/src/application/workers/jobs.worker.ts index ee5a74a1..93e6af93 100644 --- a/apps/rowboat/src/application/workers/jobs.worker.ts +++ b/apps/rowboat/src/application/workers/jobs.worker.ts @@ -8,6 +8,8 @@ import { IPubSubService, Subscription } from "../services/pub-sub.service.interf import { nanoid } from "nanoid"; import { z } from "zod"; import { PrefixLogger } from "@/app/lib/utils"; +import { IUsageQuotaPolicy } from "../policies/usage-quota.policy.interface"; +import { QuotaExceededError } from "@/src/entities/errors/common"; export interface IJobsWorker { run(): Promise; @@ -20,6 +22,7 @@ export class JobsWorker implements IJobsWorker { private readonly createConversationUseCase: ICreateConversationUseCase; private readonly runConversationTurnUseCase: IRunConversationTurnUseCase; private readonly pubSubService: IPubSubService; + private readonly usageQuotaPolicy: IUsageQuotaPolicy; private workerId: string; private subscription: Subscription | null = null; private isRunning: boolean = false; @@ -33,18 +36,21 @@ export class JobsWorker implements IJobsWorker { createConversationUseCase, runConversationTurnUseCase, pubSubService, + usageQuotaPolicy, }: { jobsRepository: IJobsRepository; projectsRepository: IProjectsRepository; createConversationUseCase: ICreateConversationUseCase; runConversationTurnUseCase: IRunConversationTurnUseCase; pubSubService: IPubSubService; + usageQuotaPolicy: IUsageQuotaPolicy; }) { this.jobsRepository = jobsRepository; this.projectsRepository = projectsRepository; this.createConversationUseCase = createConversationUseCase; this.runConversationTurnUseCase = runConversationTurnUseCase; this.pubSubService = pubSubService; + this.usageQuotaPolicy = usageQuotaPolicy; this.workerId = nanoid(); this.logger = new PrefixLogger(`jobs-worker-[${this.workerId}]`); } @@ -52,7 +58,7 @@ export class JobsWorker implements IJobsWorker { async processJob(job: z.infer): Promise { const logger = this.logger.child(`job-${job.id}`); logger.log('Processing job'); - + try { // extract project id from job const { projectId } = job; @@ -63,6 +69,9 @@ export class JobsWorker implements IJobsWorker { throw new Error("Project not found"); } + // check job-run quota usage + await this.usageQuotaPolicy.assertAndConsumeRunJobAction(projectId); + // create conversation logger.log('Creating conversation'); const conversation = await this.createConversationUseCase.execute({ @@ -114,6 +123,18 @@ export class JobsWorker implements IJobsWorker { }); logger.log(`Completed successfully`); } catch (error) { + if (error instanceof QuotaExceededError) { + logger.log(`Failed due to quota exceeded`); + + // update job + await this.jobsRepository.update(job.id, { + status: "failed", + output: { + error: (error instanceof QuotaExceededError) ? error.message : "Usage quota exceeded.", + }, + }); + return; + } logger.log(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`); // update job @@ -174,7 +195,7 @@ export class JobsWorker implements IJobsWorker { } logger.log(`Found job ${job.id} via polling`); - + // process job await this.processJob(job); } catch (error) { @@ -185,7 +206,7 @@ export class JobsWorker implements IJobsWorker { private async startPolling(): Promise { const logger = this.logger.child(`start-polling`); logger.log("Starting polling mechanism"); - + const scheduleNextPoll = () => { this.pollTimeoutId = setTimeout(async () => { await this.pollForJobs(); diff --git a/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts b/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts index 29fe2d08..4b97ace0 100644 --- a/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts +++ b/apps/rowboat/src/infrastructure/policies/redis.usage-quota.policy.ts @@ -1,12 +1,13 @@ import { IUsageQuotaPolicy } from "@/src/application/policies/usage-quota.policy.interface"; import { redisClient } from "@/app/lib/redis"; import { QuotaExceededError } from "@/src/entities/errors/common"; -import { secondsToNextMinute } from "@/src/application/lib/utils/time-to-next-minute"; +import { secondsToNextMinute, minutesToNextHour } from "@/src/application/lib/utils/time-to-next-minute"; const MAX_QUERIES_PER_MINUTE = Number(process.env.MAX_QUERIES_PER_MINUTE) || 0; +const MAX_JOBS_PER_HOUR = Number(process.env.MAX_JOBS_PER_HOUR) || 0; export class RedisUsageQuotaPolicy implements IUsageQuotaPolicy { - async assertAndConsume(projectId: string): Promise { + async assertAndConsumeProjectAction(projectId: string): Promise { if (MAX_QUERIES_PER_MINUTE === 0) { return; } @@ -23,4 +24,22 @@ export class RedisUsageQuotaPolicy implements IUsageQuotaPolicy { throw new QuotaExceededError(`Quota exceeded for project ${projectId}`); } } + + async assertAndConsumeRunJobAction(projectId: string): Promise { + if (MAX_JOBS_PER_HOUR === 0) { + return; + } + + const hour_of_the_day = new Date().getHours(); + const key = `jobs_limit:${projectId}:${hour_of_the_day}`; + + const count = await redisClient.incr(key); + if (count === 1) { + await redisClient.expire(key, minutesToNextHour() * 60); // Set TTL to clean up automatically + } + + if (count > MAX_JOBS_PER_HOUR) { + throw new QuotaExceededError(`Jobs quota exceeded for project ${projectId}`); + } + } } \ No newline at end of file From aef3771cd5da9daa8e049ff5d029b59305138a07 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Thu, 21 Aug 2025 07:43:21 +0530 Subject: [PATCH 04/15] allow copilot to delete things, set start agent --- apps/rowboat/app/lib/client_utils.ts | 8 +- .../copilot/components/actions.tsx | 33 ++++--- .../copilot/components/messages.tsx | 45 ++++++++-- .../lib/copilot/example_multi_agent_1.ts | 88 +++++++++++++++++++ .../src/application/lib/copilot/types.ts | 4 +- 5 files changed, 156 insertions(+), 22 deletions(-) diff --git a/apps/rowboat/app/lib/client_utils.ts b/apps/rowboat/app/lib/client_utils.ts index eea2bffb..90997d02 100644 --- a/apps/rowboat/app/lib/client_utils.ts +++ b/apps/rowboat/app/lib/client_utils.ts @@ -1,9 +1,11 @@ import { WorkflowTool, WorkflowAgent, WorkflowPrompt, WorkflowPipeline } from "./types/workflow_types"; import { z } from "zod"; +const ZFallbackSchema = z.object({}).passthrough(); + export function validateConfigChanges(configType: string, configChanges: Record, name: string) { let testObject: any; - let schema: z.ZodType; + let schema: z.ZodType = ZFallbackSchema; switch (configType) { case 'tool': { @@ -56,6 +58,10 @@ export function validateConfigChanges(configType: string, configChanges: Record< schema = WorkflowPipeline; break; } + case 'start_agent': { + testObject = {}; + break; + } default: return { error: `Unknown config type: ${configType}` }; } diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx index 9f1c3bdc..001bb89c 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx @@ -51,9 +51,12 @@ export function Action({ const appliedFields = Object.keys(action.config_changes).filter(key => appliedChanges[getAppliedChangeKey(msgIndex, actionIndex, key)] ); - const allApplied = externallyApplied || Object.keys(action.config_changes).every(key => + let allApplied = externallyApplied || Object.keys(action.config_changes).every(key => appliedFields.includes(key) ); + if (!externallyApplied && (action.action === "delete" || action.config_type === 'start_agent')) { + allApplied = false; + } // Handle applying a single field change const handleFieldChange = (field: string) => { @@ -160,7 +163,8 @@ export function Action({ 'transition-shadow duration-150', { 'border-l-2 border-l-blue-500': !stale && !allApplied && action.action == 'create_new', - 'border-l-2 border-l-orange-500': !stale && !allApplied && action.action == 'edit', + 'border-l-2 border-l-yellow-500': !stale && !allApplied && action.action == 'edit', + 'border-l-2 border-l-red-500': !stale && !allApplied && action.action == 'delete', 'border-l-2 border-l-gray-400': stale || allApplied || action.error, } )}> @@ -171,14 +175,15 @@ export function Action({ 'inline-flex items-center justify-center rounded-full h-5 w-5 text-xs', { 'bg-blue-100 text-blue-600': action.action == 'create_new', - 'bg-orange-100 text-orange-600': action.action == 'edit', + 'bg-yellow-100 text-yellow-600': action.action == 'edit', + 'bg-red-100 text-red-600': action.action == 'delete', 'bg-gray-200 text-gray-600': stale || allApplied || action.error, } )}> - {action.config_type === 'agent' ? '🧑‍💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : '💬'} + {action.config_type === 'agent' ? '🧑‍💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : action.config_type === 'start_agent' ? '🏁' : action.config_type === 'prompt' ? '💬' : '💬'} - {action.action === 'create_new' ? 'Add' : 'Edit'} {action.config_type}: {action.name} + {action.action === 'create_new' ? 'Add' : action.action === 'edit' ? 'Edit' : 'Delete'} {action.config_type}: {action.name} {/* Action buttons - compact, icon only, show text on hover */}
@@ -195,13 +200,13 @@ export function Action({ {allApplied ? 'Applied' : 'Apply'} - + }
{/* Description of what happened */} @@ -341,8 +346,8 @@ export function StreamingAction({ loading, }: { action: { - action?: 'create_new' | 'edit'; - config_type?: 'tool' | 'agent' | 'prompt' | 'pipeline'; + action?: 'create_new' | 'edit' | 'delete'; + config_type?: 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent'; name?: string; }; loading: boolean; @@ -354,7 +359,8 @@ export function StreamingAction({ 'transition-shadow duration-150', { 'border-l-2 border-l-blue-500': action.action == 'create_new', - 'border-l-2 border-l-orange-500': action.action == 'edit', + 'border-l-2 border-l-yellow-500': action.action == 'edit', + 'border-l-2 border-l-red-500': action.action == 'delete', 'border-l-2 border-l-gray-400': !action.action, } )}> @@ -364,14 +370,15 @@ export function StreamingAction({ 'inline-flex items-center justify-center rounded-full h-5 w-5 text-xs', { 'bg-blue-100 text-blue-600': action.action == 'create_new', - 'bg-orange-100 text-orange-600': action.action == 'edit', + 'bg-yellow-100 text-yellow-600': action.action == 'edit', + 'bg-red-100 text-red-600': action.action == 'delete', 'bg-gray-200 text-gray-600': !action.action, } )}> - {action.config_type === 'agent' ? '🧑‍💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : '💬'} + {action.config_type === 'agent' ? '🧑‍💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : action.config_type === 'start_agent' ? '🏁' : '💬'} - {action.action === 'create_new' ? 'Add' : 'Edit'} {action.config_type}: {action.name} + {action.action === 'create_new' ? 'Add' : action.action === 'edit' ? 'Edit' : 'Delete'} {action.config_type}: {action.name} {/* Loading state body */} diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx index 54fcf160..563bf798 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx @@ -70,8 +70,8 @@ function enrich(response: string): z.infer { return { type: 'action', action: { - action: metadata.action as 'create_new' | 'edit', - config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline', + action: metadata.action as 'create_new' | 'edit' | 'delete', + config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent', name: metadata.name, change_description: jsonData.change_description || '', config_changes: {}, @@ -83,8 +83,8 @@ function enrich(response: string): z.infer { return { type: 'action', action: { - action: metadata.action as 'create_new' | 'edit', - config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline', + action: metadata.action as 'create_new' | 'edit' | 'delete', + config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent', name: metadata.name, change_description: jsonData.change_description || '', config_changes: result.changes @@ -99,8 +99,8 @@ function enrich(response: string): z.infer { return { type: 'streaming_action', action: { - action: (metadata.action as 'create_new' | 'edit') || undefined, - config_type: (metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline') || undefined, + action: (metadata.action as 'create_new' | 'edit' | 'delete') || undefined, + config_type: (metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent') || undefined, name: metadata.name } }; @@ -289,6 +289,39 @@ function AssistantMessage({ pipeline: action.config_changes }); break; + case 'start_agent': + dispatch({ + type: 'set_main_agent', + name: action.name, + }) + break; + } + } else if (action.action === 'delete') { + switch (action.config_type) { + case 'agent': + dispatch({ + type: 'delete_agent', + name: action.name + }); + break; + case 'tool': + dispatch({ + type: 'delete_tool', + name: action.name + }); + break; + case 'prompt': + dispatch({ + type: 'delete_prompt', + name: action.name + }); + break; + case 'pipeline': + dispatch({ + type: 'delete_pipeline', + name: action.name + }); + break; } } }, [dispatch, workflow.agents, workflow.tools]); diff --git a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts index 5aef81e3..946e48b7 100644 --- a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts +++ b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts @@ -990,4 +990,92 @@ This workflow is now ready. Once you apply the changes, it will automatically ha --- +### Example 7: Setting the start agent + +**User Request** +Can you set the start agent to the Meeting Prep Hub? + +**Copilot Response** + +Yes, I can set the start agent to the Meeting Prep Hub. + +\`\`\`copilot_change + +// action: edit +// config_type: start_agent +// name: Meeting Prep Hub +{ + "change_description": "Set the start agent to the Meeting Prep Hub.", + "config_changes": {}, +} +\`\`\` + + +--- + +### Example 8: Delete an agent + +**User Request:** +Can you delete the Slack Send Agent? + +**Copilot Response:** + +Yes, I can delete the Slack Send Agent. + +\`\`\`copilot_change +// action: delete +// config_type: agent +// name: Slack Send Agent +{ + "change_description": "Delete the Slack Send Agent.", + "config_changes": {}, + } +} +\`\`\` + +--- + +### Example 9: Delete a tool + +**User Request:** +Can you delete the Search tool? + +**Copilot Response:** + +Yes, I can delete the Search tool. + + +\`\`\`copilot_change +// action: delete +// config_type: tool +// name: Search +{ + "change_description": "Delete the Search tool.", + "config_changes": {}, +} +\`\`\` + +--- + +### Example 10: Delete a pipeline + +**User Request:** +Can you delete the Meeting Prep Pipeline? + +**Copilot Response:** + +Yes, I can delete the Meeting Prep Pipeline. + +\`\`\`copilot_change +// action: delete +// config_type: pipeline +// name: Meeting Prep Pipeline +{ + "change_description": "Delete the Meeting Prep Pipeline.", + "config_changes": {}, +} +\`\`\` + +--- + `; \ No newline at end of file diff --git a/apps/rowboat/src/application/lib/copilot/types.ts b/apps/rowboat/src/application/lib/copilot/types.ts index 5d42b0e8..5b54f9dc 100644 --- a/apps/rowboat/src/application/lib/copilot/types.ts +++ b/apps/rowboat/src/application/lib/copilot/types.ts @@ -21,8 +21,8 @@ export const CopilotAssistantMessageTextPart = z.object({ export const CopilotAssistantMessageActionPart = z.object({ type: z.literal("action"), content: z.object({ - config_type: z.union([z.literal('tool'), z.literal('agent'), z.literal('prompt'), z.literal('pipeline')]), - action: z.union([z.literal('create_new'), z.literal('edit')]), + config_type: z.enum(['tool', 'agent', 'prompt', 'pipeline', 'start_agent']), + action: z.enum(['create_new', 'edit', 'delete']), name: z.string(), change_description: z.string(), config_changes: z.record(z.string(), z.unknown()), From 291943cdd57f8e89a62fffd9f7975327065f0621 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 21 Aug 2025 08:48:57 +0530 Subject: [PATCH 05/15] Fix copilot welcome screen component overlay --- .../app/projects/[projectId]/copilot/app.tsx | 54 ++++++++++++++++++- .../copilot/components/messages.tsx | 2 +- .../components/common/panel-common.tsx | 54 ++----------------- 3 files changed, 57 insertions(+), 53 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 859aea90..538b6070 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -14,6 +14,9 @@ import { Messages } from "./components/messages"; import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon, Sparkles } from "lucide-react"; import { useCopilot } from "./use-copilot"; import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; +import { SHOW_COPILOT_MARQUEE } from "@/app/lib/feature_flags"; +import Image from "next/image"; +import mascot from "@/public/mascot.png"; const CopilotContext = createContext<{ workflow: z.infer | null; @@ -205,6 +208,56 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
+ {messages.length === 0 && ( +
+ {/* Replace Sparkles icon with mascot image */} + Rowboat Mascot + + {/* Welcome/Intro Section */} +
+

+ 👋 Welcome to Rowboat! +

+

+ I'm your copilot for building agents and adding tools to them. +

+

+ Here's what you can do in Rowboat: +

+
+
+ + Build AI agents instantly with natural language. +
+
+ 🔌 + Connect tools with one-click integrations. +
+
+ 📂 + Power with knowledge by adding documents for RAG. +
+
+ 🔄 + Automate workflows by setting up triggers and actions. +
+
+ 🚀 + Deploy anywhere via API or SDK. +
+
+
+ + {SHOW_COPILOT_MARQUEE && ( +
+
+
What can I help you build?
+
 
+
+
+ )} +
+ )} void } title="Skipper" subtitle="Build your assistant" diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx index 563bf798..2b891084 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx @@ -575,7 +575,7 @@ export function Messages({ }; return ( -
+
{displayMessages.map((message, index) => (
diff --git a/apps/rowboat/components/common/panel-common.tsx b/apps/rowboat/components/common/panel-common.tsx index 1ceb8208..3ee7875f 100644 --- a/apps/rowboat/components/common/panel-common.tsx +++ b/apps/rowboat/components/common/panel-common.tsx @@ -81,56 +81,6 @@ export function Panel({ onClick={onClick} data-tour-target={tourTarget} > - {variant === 'copilot' && showWelcome && ( -
- {/* Replace Sparkles icon with mascot image */} - Rowboat Mascot - - {/* Welcome/Intro Section */} -
-

- 👋 Welcome to Rowboat! -

-

- I'm your copilot for building agents and adding tools to them. -

-

- Here's what you can do in Rowboat: -

-
-
- - Build AI agents instantly with natural language. -
-
- 🔌 - Connect tools with one-click integrations. -
-
- 📂 - Power with knowledge by adding documents for RAG. -
-
- 🔄 - Automate workflows by setting up triggers and actions. -
-
- 🚀 - Deploy anywhere via API or SDK. -
-
-
- - {SHOW_COPILOT_MARQUEE && ( -
-
-
What can I help you build?
-
 
-
-
- )} -
- )}
{children}
- ) : children} + ) : ( + children + )}
; } \ No newline at end of file From 4876b8e818767407f7abd3cb3d8ebbc76778fe81 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 21 Aug 2025 09:08:34 +0530 Subject: [PATCH 06/15] Re-route help button to discord --- .../projects/layout/components/sidebar.tsx | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/rowboat/app/projects/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx index 739d3821..1713a74f 100644 --- a/apps/rowboat/app/projects/layout/components/sidebar.tsx +++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx @@ -256,24 +256,27 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl {/* Theme and Auth Controls */}
- {USE_PRODUCT_TOUR && !isProjectsRoute && ( - - - - )} + {/* Help button - always visible, but behavior depends on feature flag */} + + + {SHOW_DARK_MODE_TOGGLE && ( From e68da3de21bb8674a8805562882febd72b3131c8 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 21 Aug 2025 09:49:24 +0530 Subject: [PATCH 07/15] Move agent name to header of agent config --- .../[projectId]/entities/agent_config.tsx | 108 +++++++++++------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index f44432f4..0ccf3435 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -2,10 +2,10 @@ import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types"; import { DataSource } from "@/src/entities/models/data-source"; import { z } from "zod"; -import { PlusIcon, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings, Info } from "lucide-react"; +import { PlusIcon, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings, Info, Edit3 } from "lucide-react"; import { useState, useEffect, useRef } from "react"; import { usePreviewModal } from "../workflow/preview-modal"; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react"; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection, Input } from "@heroui/react"; import { PreviewModalProvider } from "../workflow/preview-modal"; import { CopilotMessage } from "@/src/application/lib/copilot/types"; import { getCopilotAgentInstructions } from "@/app/actions/copilot.actions"; @@ -17,7 +17,6 @@ import { Button as CustomButton } from "@/components/ui/button"; import clsx from "clsx"; import { InputField } from "@/app/lib/components/input-field"; import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags"; -import { Input } from "@/components/ui/input"; import { Info as InfoIcon } from "lucide-react"; import { useCopilot } from "../copilot/use-copilot"; import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; @@ -78,6 +77,8 @@ export function AgentConfig({ const [previousRagSources, setPreviousRagSources] = useState([]); const [billingError, setBillingError] = useState(null); const [showSavedBanner, setShowSavedBanner] = useState(false); + const [isEditingName, setIsEditingName] = useState(false); + const nameInputRef = useRef(null); // Check if this agent is a pipeline agent const isPipelineAgent = agent.type === 'pipeline'; @@ -101,6 +102,14 @@ export function AgentConfig({ setLocalName(agent.name); }, [agent.name]); + // Focus name input when entering edit mode + useEffect(() => { + if (isEditingName && nameInputRef.current) { + nameInputRef.current.focus(); + nameInputRef.current.select(); + } + }, [isEditingName]); + // Track changes in RAG datasources useEffect(() => { const currentSources = agent.ragDataSources || []; @@ -188,15 +197,36 @@ export function AgentConfig({ return true; }; - const handleNameChange = (e: React.ChangeEvent) => { + const handleNameChange = (e: React.ChangeEvent) => { const newName = e.target.value; setLocalName(newName); - - if (validateName(newName)) { + setNameError(null); + }; + + const handleNameCommit = () => { + if (validateName(localName)) { handleUpdate({ ...agent, - name: newName + name: localName }); + showSavedMessage(); + setIsEditingName(false); + } + }; + + const handleNameCancel = () => { + setLocalName(agent.name); + setNameError(null); + setIsEditingName(false); + }; + + const handleNameKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleNameCommit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleNameCancel(); } }; @@ -221,8 +251,36 @@ export function AgentConfig({ -
- {agent.name} +
+ {isEditingName ? ( +
+ +
+ ) : ( + + )}
- {/* Identity Section Card */} - } - title="Identity" - labelWidth="md:w-32" - className="mb-1" - > -
-
- -
- { - setLocalName(value); - if (validateName(value)) { - handleUpdate({ - ...agent, - name: value - }); - } - showSavedMessage(); - }} - error={nameError} - className="w-full" - /> -
-
- -
-
{/* Behavior Section Card */} } From ab0049d9ba67514480f5be33bee0304cae904dab Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 21 Aug 2025 09:58:28 +0530 Subject: [PATCH 08/15] Update configurations tab name and remove max calls from parent --- .../[projectId]/entities/agent_config.tsx | 55 ++++--------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index 0ccf3435..15abca57 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -158,6 +158,13 @@ export function AgentConfig({ } }, [agent.controlType, agent.outputVisibility, agent, handleUpdate]); + // Add effect to ensure internal agents have maxCallsPerParentAgent set to 1 by default + useEffect(() => { + if (agent.outputVisibility === "internal" && !isPipelineAgent && agent.maxCallsPerParentAgent === undefined) { + handleUpdate({ ...agent, maxCallsPerParentAgent: 1 }); + } + }, [agent.outputVisibility, agent.maxCallsPerParentAgent, agent, handleUpdate, isPipelineAgent]); + // Add effect to handle escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -239,13 +246,7 @@ export function AgentConfig({ currentAgent: agent }); - // Add local state for max calls input - const [maxCallsInput, setMaxCallsInput] = useState(String(agent.maxCallsPerParentAgent || 3)); - const [maxCallsError, setMaxCallsError] = useState(null); - // Sync local state with agent prop - useEffect(() => { - setMaxCallsInput(String(agent.maxCallsPerParentAgent || 3)); - }, [agent.maxCallsPerParentAgent]); + return ( - {tab.charAt(0).toUpperCase() + tab.slice(1)} + {tab === 'instructions' ? 'Instructions' : 'Model & RAG'} ))}
@@ -644,43 +645,7 @@ export function AgentConfig({ }
- {agent.outputVisibility === "internal" && !isPipelineAgent && ( -
- -
- { - setMaxCallsInput(value); - setMaxCallsError(null); - const num = Number(value); - if (value && !isNaN(num) && num >= 1 && Number.isInteger(num)) { - if (num !== agent.maxCallsPerParentAgent) { - handleUpdate({ - ...agent, - maxCallsPerParentAgent: num - }); - } - } - }} - validate={(value: string) => { - const num = Number(value); - if (!value || isNaN(num) || num < 1 || !Number.isInteger(num)) { - return { valid: false, errorMessage: "Must be an integer >= 1" }; - } - return { valid: true }; - }} - error={maxCallsError} - min={1} - className="w-full max-w-24" - /> - {maxCallsError && ( -

{maxCallsError}

- )} -
-
- )} + {USE_TRANSFER_CONTROL_OPTIONS && !isPipelineAgent && (
From 981cff3b3f47c02280baac35202ea4a46eae21df Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 21 Aug 2025 10:15:28 +0530 Subject: [PATCH 09/15] Update copilot welcome message --- apps/rowboat/app/projects/[projectId]/copilot/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 538b6070..fc9675fe 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -216,7 +216,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message {/* Welcome/Intro Section */}

- 👋 Welcome to Rowboat! + 👋 Hi there!

I'm your copilot for building agents and adding tools to them. From ab014f788cd162af840948c585d9c081cb234ce3 Mon Sep 17 00:00:00 2001 From: arkml Date: Thu, 21 Aug 2025 17:29:27 +0530 Subject: [PATCH 10/15] Context variables (#214) * move prompts panel to variables * variable shows both name and value * added modal to add variables * removed warning on edits * adding or updating variables only uses the modal * append variable context to agent instructions * add dummy value to the variable values when downloading json * fixed @variable mentions in the instruction editor * change placeholder text for variables when json is imported --- apps/rowboat/app/lib/components/atmentions.ts | 8 +- apps/rowboat/app/lib/feature_flags.ts | 2 +- apps/rowboat/app/lib/types/workflow_types.ts | 14 +- .../[projectId]/entities/prompt_config.tsx | 9 +- .../[projectId]/workflow/entity_list.tsx | 210 +++++++++++++++++- .../[projectId]/workflow/workflow_editor.tsx | 104 ++++++++- .../lib/agents-runtime/agent_instructions.ts | 22 +- .../application/lib/agents-runtime/agents.ts | 12 +- 8 files changed, 358 insertions(+), 23 deletions(-) diff --git a/apps/rowboat/app/lib/components/atmentions.ts b/apps/rowboat/app/lib/components/atmentions.ts index f1b94712..1b416148 100644 --- a/apps/rowboat/app/lib/components/atmentions.ts +++ b/apps/rowboat/app/lib/components/atmentions.ts @@ -55,11 +55,15 @@ export function createAtMentions({ agents, prompts, tools, pipelines = [], curre // Add prompts (always allowed) for (const prompt of prompts) { - const id = `prompt:${prompt.name}`; + // Use 'variable' for base_prompt types, 'prompt' for others + const isVariable = prompt.type === 'base_prompt'; + const type = isVariable ? 'variable' : 'prompt'; + const label = isVariable ? 'Variable' : 'Prompt'; + const id = `${type}:${prompt.name}`; atMentions.push({ id, value: id, - label: `Prompt: ${prompt.name}`, + label: `${label}: ${prompt.name}`, denotationChar: "@", link: id, target: "_self" diff --git a/apps/rowboat/app/lib/feature_flags.ts b/apps/rowboat/app/lib/feature_flags.ts index 74d4fa1c..a80df7c6 100644 --- a/apps/rowboat/app/lib/feature_flags.ts +++ b/apps/rowboat/app/lib/feature_flags.ts @@ -15,6 +15,6 @@ export const USE_VOICE_FEATURE = false; export const USE_TRANSFER_CONTROL_OPTIONS = false; export const USE_PRODUCT_TOUR = false; export const SHOW_COPILOT_MARQUEE = false; -export const SHOW_PROMPTS_SECTION = false; +export const SHOW_PROMPTS_SECTION = true; export const SHOW_DARK_MODE_TOGGLE = false; export const SHOW_VISUALIZATION = false \ No newline at end of file diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index 9c75e4c1..8f6b184f 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -129,8 +129,8 @@ export function sanitizeTextWithMentions( sanitized: string; entities: z.infer[]; } { - // Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent/pipeline - const mentionRegex = /\[@(tool|prompt|agent|pipeline):([^\]]+)\]\(#mention\)/g; + // Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent/pipeline/variable + const mentionRegex = /\[@(tool|prompt|agent|pipeline|variable):([^\]]+)\]\(#mention\)/g; const seen = new Set(); // collect entities @@ -144,8 +144,10 @@ export function sanitizeTextWithMentions( return true; }) .map(match => { + // Treat @variable: as @prompt: internally + const type = match[1] === 'variable' ? 'prompt' : match[1]; return { - type: match[1] as 'tool' | 'prompt' | 'agent' | 'pipeline', + type: type as 'tool' | 'prompt' | 'agent' | 'pipeline', name: match[2], }; }) @@ -176,6 +178,12 @@ export function sanitizeTextWithMentions( const id = `${entity.type}:${entity.name}`; const textToReplace = `[@${id}](#mention)`; text = text.replace(textToReplace, `[@${id}]`); + + // Also handle @variable: mentions for prompts + if (entity.type === 'prompt') { + const variableTextToReplace = `[@variable:${entity.name}](#mention)`; + text = text.replace(variableTextToReplace, `[@variable:${entity.name}]`); + } } return { diff --git a/apps/rowboat/app/projects/[projectId]/entities/prompt_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/prompt_config.tsx index 159bd7b9..1ae2d998 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/prompt_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/prompt_config.tsx @@ -14,6 +14,9 @@ const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider // Enhanced textarea styles with improved states const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500"; +// Value field styles without grey placeholder text +const valueTextareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-black dark:placeholder:text-white"; + export function PromptConfig({ prompt, agents, @@ -128,7 +131,7 @@ export function PromptConfig({