Run notifications reached at most one phone and nothing could hold them back at night. - PushNotificationProvider sends each notification to all of the user's registered devices (web push and Expo), skips everything during quiet hours in the profile time zone, drops devices the push service reports gone (404/410, DeviceNotRegistered), records lastUsedAt and links each push to its conversation. - Expo tokens move from DATA_DIR/push-tokens files into push_subscriptions: the API imports and deletes the legacy files at start; notifications.registerPush/unregisterPush keep their contract for the mobile app (tokens must look like Expo tokens). - NotificationMessage gains the nudge kind, and the main bot gets notify_user: a short push to the main conversation that respects the nudge switch, quiet hours and a cap of three per day. It is withheld from shared messaging channels and from runs answering another owner's agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
import type { ConnectorTool } from "@rakazo/adapter-kit";
|
|
import { agentConnectionTools, builtinAgentTools } from "./builtin-tools.js";
|
|
import { selectCloudAgentTools } from "./cloud-agent-tools-select.js";
|
|
import { selectMemoryTools } from "./memory-tools.js";
|
|
import { filterImageReturningComputerTools } from "./model-vision.js";
|
|
import {
|
|
filterBuiltinToolsForRun,
|
|
filterBuiltinToolsForThread,
|
|
withScheduleTargetBot,
|
|
} from "./schedule-tools.js";
|
|
|
|
/** Browser tools that require a graphical computer (Chrome). */
|
|
export const PAGE_BROWSER_TOOL_NAMES = new Set([
|
|
"browser_navigate",
|
|
"browser_snapshot",
|
|
"browser_act",
|
|
]);
|
|
|
|
/** Builtin tools only the user's main bot may use; every other bot's runs never see them. */
|
|
export const MAIN_ONLY_BUILTIN_TOOLS: ReadonlySet<string> = new Set<string>([
|
|
"delegate_task",
|
|
"notify_user",
|
|
"update_user_profile",
|
|
]);
|
|
|
|
const BUILTIN_TOOL_NAMES = [...builtinAgentTools, ...agentConnectionTools].map((tool) => tool.name);
|
|
|
|
/** The bot fields that decide which builtin tools its runs may use. */
|
|
export interface BuiltinToolPolicy {
|
|
kind: string;
|
|
restrictBuiltinTools: boolean;
|
|
allowedBuiltinTools: readonly string[];
|
|
}
|
|
|
|
/**
|
|
* Builtin tool names a bot's allowlist can choose from, in catalog order. Agent connection tools
|
|
* only ever reach chat-linked bots, so only those list them.
|
|
*/
|
|
export function builtinToolNamesForBot(bot: { kind: string; chatLinked: boolean }): string[] {
|
|
return [...builtinAgentTools, ...(bot.chatLinked ? agentConnectionTools : [])]
|
|
.map((tool) => tool.name)
|
|
.filter((name) => bot.kind === "main" || !MAIN_ONLY_BUILTIN_TOOLS.has(name));
|
|
}
|
|
|
|
/** Names that are not builtin tools; an allowlist must not carry typos or connector tools. */
|
|
export function unknownBuiltinToolNames(names: readonly string[]): string[] {
|
|
return names.filter((name) => !BUILTIN_TOOL_NAMES.includes(name));
|
|
}
|
|
|
|
/**
|
|
* Least privilege per bot: main-only tools stay with the main bot, a restricted bot keeps only
|
|
* its allowlist (empty means none), and the main bot may schedule for another bot.
|
|
*/
|
|
export function filterBuiltinToolsForBot(
|
|
tools: ConnectorTool[],
|
|
bot: BuiltinToolPolicy,
|
|
): ConnectorTool[] {
|
|
const main = bot.kind === "main";
|
|
const allowed = new Set(bot.allowedBuiltinTools);
|
|
return tools
|
|
.filter(
|
|
(tool) =>
|
|
(main || !MAIN_ONLY_BUILTIN_TOOLS.has(tool.name)) &&
|
|
(!bot.restrictBuiltinTools || allowed.has(tool.name)),
|
|
)
|
|
.map((tool) => (main && tool.name === "schedule_create" ? withScheduleTargetBot(tool) : tool));
|
|
}
|
|
|
|
export function filterPageBrowserTools<T extends { name: string }>(
|
|
tools: T[],
|
|
pageBrowserAllowed: boolean,
|
|
): T[] {
|
|
if (pageBrowserAllowed) return tools;
|
|
return tools.filter((tool) => !PAGE_BROWSER_TOOL_NAMES.has(tool.name));
|
|
}
|
|
|
|
export function selectBuiltinToolsForRun(options: {
|
|
graphicalToolsAllowed: boolean;
|
|
/** Page browser tools need a graphical computer (Chrome), not model vision. */
|
|
pageBrowserAllowed?: boolean;
|
|
groupId: string | null;
|
|
trigger: string;
|
|
semanticMemoryEnabled: boolean;
|
|
cloudAgentEnabled?: boolean;
|
|
messagingChannelRun: boolean;
|
|
/** Cross-owner agent connections only exist for chat-linked bots. */
|
|
agentConnectionsEnabled?: boolean;
|
|
/** Answering another owner's agent: what the run hears is not the user, so no profile writes. */
|
|
foreignAgentRun?: boolean;
|
|
bot: BuiltinToolPolicy;
|
|
}) {
|
|
const selected = selectCloudAgentTools(
|
|
selectMemoryTools(
|
|
filterBuiltinToolsForRun(
|
|
filterBuiltinToolsForThread(
|
|
filterPageBrowserTools(
|
|
filterImageReturningComputerTools(builtinAgentTools, options.graphicalToolsAllowed),
|
|
options.pageBrowserAllowed ?? options.graphicalToolsAllowed,
|
|
),
|
|
options.groupId,
|
|
),
|
|
options.trigger,
|
|
),
|
|
options.semanticMemoryEnabled,
|
|
),
|
|
Boolean(options.cloudAgentEnabled),
|
|
).filter(
|
|
(tool) =>
|
|
!(
|
|
options.foreignAgentRun &&
|
|
(tool.name === "update_user_profile" || tool.name === "notify_user")
|
|
) &&
|
|
(!options.messagingChannelRun ||
|
|
(![
|
|
"remember",
|
|
"save_memory",
|
|
"recall_memory",
|
|
"forget_memory",
|
|
"update_user_profile",
|
|
"notify_user",
|
|
].includes(tool.name) &&
|
|
!tool.name.startsWith("scratchpad_"))),
|
|
);
|
|
return filterBuiltinToolsForBot(
|
|
options.agentConnectionsEnabled ? [...selected, ...agentConnectionTools] : selected,
|
|
options.bot,
|
|
);
|
|
}
|