Repository navigation
feat: implement Nova automation features and enhance API integrations: - #326
Conversation
- Introduced new API routes for managing automations, including creation, listing, updating, and deletion. - Added support for webhook triggers and automation event processing, allowing for dynamic responses to external events. - Implemented automation context retrieval and user authorization checks to ensure secure access to automation functionalities. - Enhanced automation input validation and error handling for improved reliability and user experience. - Created utility functions for managing automation templates and triggers, streamlining the automation setup process. - Integrated automation event normalization for Slack, GitHub, and Linear, enabling seamless interaction with external services.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (35)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Changed the import of Prisma to explicitly include the type keyword for better type inference. - Updated related files in the automation module and database index to maintain consistency in type usage.
|
| const { token } = await ensureHarnessToken({ ...member.user, background: true }) | ||
| let text = "" | ||
| let finished = false | ||
| for await (const part of streamHarnessChat({ token, model: "gemini-2.5-flash", provider: "supercode", signal: AbortSignal.timeout(45_000), messages: [ |
There was a problem hiding this comment.
classifyAutomationRun requests gemini-2.5-flash through supercode, but that provider rejects models outside CLOUD_ALLOWED_MODELS, which excludes this model. Every automation with nonempty onlyRunWhen, including every enabled template, fails before checking the event. Changing the execution model does not help because this request is fixed. Use an allowed cloud model here.
| for await (const part of streamHarnessChat({ token, model: "gemini-2.5-flash", provider: "supercode", signal: AbortSignal.timeout(45_000), messages: [ | |
| for await (const part of streamHarnessChat({ token, model: "deepseek-v4-flash", provider: "supercode", signal: AbortSignal.timeout(45_000), messages: [ |
| instructions: template?.instructions ?? "", | ||
| onlyRunWhen: template?.onlyRunWhen ?? "", | ||
| triggers: [defaultTrigger(template?.type ?? "slack.message")], | ||
| model: "x-ai/grok-4-5", provider: "concentrateai", effort: "medium", |
There was a problem hiding this comment.
templateDraft defaults every new automation to concentrateai and x-ai/grok-4-5, but streamHarnessChat never sends concentrateAiKey. Without that key, the server restricts this provider to CLOUD_ALLOWED_MODELS, which excludes this model. Even with empty criteria, a new automation fails at execution unless the user changes its model. Default to a model the web request can call.
| model: "x-ai/grok-4-5", provider: "concentrateai", effort: "medium", | |
| model: "deepseek-v4-flash", provider: "supercode", effort: "medium", |
| const { token } = await ensureHarnessToken({ ...member.user, background: true }) | ||
| let body = "" | ||
| let finished = false | ||
| for await (const part of streamHarnessAgent({ token, model: input.model, provider: input.provider, signal: AbortSignal.timeout(240_000), messages: [ |
There was a problem hiding this comment.
Automations can change connected apps
executeAutomationRun omits mode, so streamHarnessAgent defaults to agent, offers write tools, and appends instructions allowing mutations. This contradicts the page’s read-only promise and externalPosting: false. With a callable model and empty criteria, third-party event text reaches a write-enabled agent running as the selected account. Pass a read-only mode and enforce it when tools execute.
How this was verified: Automation event text reaches streamHarnessAgent without a mode, and its default policy allows connected write calls through to executeTool.
| for await (const part of streamHarnessAgent({ token, model: input.model, provider: input.provider, signal: AbortSignal.timeout(240_000), messages: [ | |
| for await (const part of streamHarnessAgent({ token, model: input.model, provider: input.provider, mode: "chat", signal: AbortSignal.timeout(240_000), messages: [ |
| const missingApps = appsLoaded && !appsError | ||
| ? requiredProviders.filter((provider) => !apps.some((app) => app.slug === provider.toLowerCase() && app.connected)) | ||
| : [] |
There was a problem hiding this comment.
Setup misses required connections
The banner checks only Composio accounts and tells users to connect providers through Composio. Provider triggers require an active externalInstallation, which that connection flow never creates. A user can follow the instructions, clear the banner, and save an automation whose provider events are still ignored. Check context.connections for trigger readiness and guide users through native connector setup separately from tool access.
| const ranked = preferTools(catalog.tools, mode).slice(0, MAX_TOOLS_TO_MODEL) | ||
| const composioTools: HarnessChatTool[] = ranked.map((tool) => ({ | ||
| type: "function", | ||
| function: { name: tool.name, description: tool.description, parameters: tool.parameters }, | ||
| })) | ||
| const toolkits = [...new Set(readTools.map((tool) => tool.toolkit))] | ||
| const extraTools = input.extraTools ?? [] | ||
| const tools: HarnessChatTool[] = [...extraTools, ...composioTools] | ||
| const toolkits = [...new Set(ranked.map((tool) => tool.toolkit).filter(Boolean))] |
There was a problem hiding this comment.
The first 64 tools are chosen by provider order, not the user’s request. If connected apps supply more than 64 allowed tools, later tools disappear with no way for the model to discover them. toolkits also comes from the shortened list, so the prompt can describe a connected app as unavailable. Keep the cap, but select tools for the request or let the model discover omitted tools.
| export async function reconcileAutomations(now = new Date()): Promise<{ queued: number; scheduled: number }> { | ||
| const pending = await prisma.novaAutomationRun.findMany({ where: { status: "queued", automation: { status: "active" } }, select: { id: true, automationId: true }, orderBy: { createdAt: "asc" }, take: 100 }) | ||
| for (const run of pending) await inngest.send({ name: "nova/automation.run", data: { automationId: run.automationId, runId: run.id } }) | ||
| const rows = await prisma.novaAutomation.findMany({ where: { status: "active" }, select: { id: true, organizationId: true, triggers: true }, take: 1000 }) |
There was a problem hiding this comment.
Dispatch skips saved automations
reconcileAutomations reads at most 1,000 active automations without paging. If there are 1,001, at least one is not checked during its scheduled minute and can miss its weekly run. Provider dispatch has the same problem at 200 automations per organization. Page through the candidates instead of silently dropping rows.
| const dayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())) | ||
| const count = await tx.novaAutomationRun.count({ where: { automationId: row.automationId, createdAt: { gte: dayStart }, status: { in: ["classifying", "running", "completed", "failed", "filtered"] } } }) |
There was a problem hiding this comment.
The daily budget counts createdAt, not when a run reserves its budget. Runs queued before midnight but claimed afterward consume today’s model calls without appearing in today’s count. With cooldownMinutes: 0, yesterday’s backlog can exceed maxRunsPerDay. Record and count the reservation time instead.
| <NavItem | ||
| active={view === "approvals"} | ||
| icon={<ShieldCheck className="size-3.5" strokeWidth={1.75} />} | ||
| label="Approvals" | ||
| badge={pendingApprovals > 0 ? pendingApprovals : undefined} | ||
| onClick={() => onNavigate("/approvals")} | ||
| active={view === "automations"} | ||
| icon={<Zap className="size-3.5" strokeWidth={1.75} />} | ||
| label="Automations" | ||
| onClick={() => onNavigate("/automations")} | ||
| /> |
There was a problem hiding this comment.
Waiting approvals lose their entry
Adding Automations removes the only navigation link and pending-count badge for Approvals. The page still exists, but users must know its URL or find each affected thread individually. Keep the Approvals entry, or move its link and count somewhere visible, so users can find jobs waiting for a decision.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const input = automationInputSchema.parse(row.snapshot) | ||
| const session = await tx.agentSession.create({ data: { organizationId: row.automation.organizationId, initiatorMembershipId: input.runAsMembershipId, objective: input.name, mode: "agent", nextSequence: 2, policySnapshot: { automationId: row.automationId, externalPosting: false } } }) | ||
| await tx.sessionSurface.create({ data: { agentSessionId: session.id, provider: "web", surfaceKey: `${row.automation.organizationId}:web:${session.id}`, externalSurfaceId: session.id } }) | ||
| await tx.agentSessionMessage.create({ data: { agentSessionId: session.id, sequence: 1, role: "user", content: input.instructions, senderType: "automation", senderId: row.automationId, metadata: { automationRunId: row.id, event: row.event } } }) |
There was a problem hiding this comment.
createAutomationSession saves the trigger event only in message metadata. Follow-up turns use buildTurnContext, which reads message content and prior responses but not metadata. The model loses the original event when a user continues the thread. Include it in the conversation context so follow-up questions can refer to the original details.
| const turnMode = resolved.mode | ||
| if (resolved.auto && turnMode !== agentMode) { | ||
| setAgentMode(turnMode) |
There was a problem hiding this comment.
Automatic mode leaves files unapplied
Auto-selection changes the mode sent to the server, but the file-apply branch still checks opts?.mode ?? agentMode. Those values belong to the old render; setAgentMode does not update this running callback. After a chat turn, a request that switches to agent can produce file proposals without applying them even when write permission is already granted. Use the resolved turnMode for that branch too.
Description
Type of change
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
bun testpassesbun run typecheckpassesbun run lintpasses (if applicable)Checklist:
Summary by CodeRabbit