import { normalizeAccessDeclaration, type WorkflowAccessDeclaration } from "../workflow-access.js"; import type { CustomAgentIntent, NotionAsCodeIcon, ResourceId } from "./intents.js"; import { recordIntent } from "./recorder.js"; import { assertUserResourceId } from "./resource.js"; import type { RecurrenceSchedule } from "./recurrence.js"; export type { RecurrenceWeekdays, MonthlyWeekdayOccurrence, MonthlyRecurrenceRestriction, RecurrenceScheduleEnd, HourlyRecurrenceSchedule, DailyRecurrenceSchedule, WeeklyRecurrenceSchedule, MonthlyRecurrenceSchedule, YearlyRecurrenceSchedule, RecurrenceSchedule, } from "./recurrence.js"; export type CustomAgentRecurrenceSchedule = RecurrenceSchedule & { type: "recurrence" }; export type CustomAgentTriggerBase = { resourceId: ResourceId; enabled?: boolean; }; export type CustomAgentRecurrenceTrigger = CustomAgentTriggerBase & CustomAgentRecurrenceSchedule; export type PropertyTriggerCondition = { type: "property_edited"; }; export type CustomAgentPageAddedTrigger = CustomAgentTriggerBase & { type: "page_added"; dataSourceResourceId: ResourceId; /** Wait for follow-up edits before running the agent. Defaults to false in Apps. */ waitForEditsToFinish?: boolean; }; type PropertyUpdatedTriggerTarget = | { propertyConditions: Readonly>; triggerWhenPageContentEdited?: boolean; } | { propertyConditions?: never; triggerWhenPageContentEdited: true; }; export type CustomAgentPropertyUpdatedTrigger = CustomAgentTriggerBase & PropertyUpdatedTriggerTarget & { type: "property_updated"; dataSourceResourceId: ResourceId; /** Wait for follow-up edits before running the agent. Defaults to false in Apps. */ waitForEditsToFinish?: boolean; }; export type CustomAgentTrigger = | CustomAgentRecurrenceTrigger | CustomAgentPageAddedTrigger | CustomAgentPropertyUpdatedTrigger; /** * Defines access levels for resources that a custom agent can use. * * These values map to Notion module permissions: full access, content editing, * commenting, and read-only. */ /** Grants a custom agent access to a page or database resource. */ export type CustomAgentAccess = Extract< WorkflowAccessDeclaration, { readonly resource: { readonly resourceType: "page" | "database" } } >; /** Trust every URL, trust no URLs, or trust a nonempty list of up to 20 URL globs. */ export type CustomAgentTrustedUrls = "all" | "none" | [string, ...string[]]; export type CustomAgentArgs = { resourceId: ResourceId; name: string; icon?: Exclude; /** Agent instructions in Notion Markdown vNext. */ instructions?: string; model?: "ambrosia-tart-high" | "opal-quince-medium" | "almond-croissant-low"; /** Whether the agent can use the Web Browser. Omission preserves the existing setting. */ browserUse?: boolean; /** * Whether the agent can use web search. Defaults to `false` when omitted for * a new agent. When updating an existing agent, omitting this field preserves * its current web access and domain restrictions. */ webAccess?: boolean; /** * Set `"all"` to trust every HTTP and HTTPS URL, `"none"` to disable URL * trust, or provide a nonempty list of up to 20 URL globs. Omission leaves * trust disabled for a new agent and preserves it for an existing agent. * URL trust is independent from web search. */ trustedUrls?: CustomAgentTrustedUrls; /** * Grants the agent access to pages and databases. * * For an existing custom agent, omit this field or provide an empty list to * preserve current page and database grants. Each listed resource adds a * grant or replaces its existing grant with the listed role. Grants for * unlisted resources remain unchanged. */ access?: readonly CustomAgentAccess[]; triggers?: readonly CustomAgentTrigger[]; }; export type CustomAgentHandle = { readonly resourceType: "customAgent"; readonly resourceId: string; }; /** Declare a custom agent and validate every resource reference. */ export function customAgent(args: CustomAgentArgs): CustomAgentHandle { assertUserResourceId(args.resourceId); const grants = args.access?.map((grant) => { const requirement = normalizeAccessDeclaration(grant, "Custom agent access"); if (requirement.type !== "page" && requirement.type !== "database") { throw new Error("Custom agent access requires a page or database handle."); } assertUserResourceId(requirement.resourceId); return { resourceId: requirement.resourceId, level: requirement.level }; }); for (const trigger of args.triggers ?? []) { assertUserResourceId(trigger.resourceId); if (trigger.type === "page_added" || trigger.type === "property_updated") { assertUserResourceId(trigger.dataSourceResourceId); } if (trigger.type === "property_updated") { for (const propertyResourceId of Object.keys(trigger.propertyConditions ?? {})) { assertUserResourceId(propertyResourceId); } } } const { access: _access, trustedUrls, triggers, ...intent } = args; recordIntent({ type: "custom_agent", ...intent, ...(trustedUrls === undefined ? {} : { trustedUrls: toTrustedUrlsIntent(trustedUrls) }), ...(triggers === undefined ? {} : { triggers: triggers.map(toTriggerIntent) }), ...(grants === undefined ? {} : { access: grants }), } satisfies { type: "custom_agent"; } & CustomAgentIntent); return { resourceType: "customAgent", resourceId: args.resourceId }; } function toTriggerIntent(trigger: CustomAgentTrigger): CustomAgentTrigger { if (trigger.type === "recurrence") { return trigger; } return { ...trigger, waitForEditsToFinish: trigger.waitForEditsToFinish ?? false }; } function toTrustedUrlsIntent( trustedUrls: CustomAgentTrustedUrls, ): NonNullable { if (trustedUrls === "all") { return { type: "all" }; } return { type: "list", urls: trustedUrls === "none" ? [] : [...trustedUrls] }; }