feat: multi-agent routing + multi-account providers
This commit is contained in:
@@ -71,7 +71,7 @@ function createMinimalRun(params?: {
|
||||
const typing = createTyping();
|
||||
const opts = params?.opts;
|
||||
const sessionCtx = {
|
||||
Surface: "whatsapp",
|
||||
Provider: "whatsapp",
|
||||
MessageSid: "msg",
|
||||
} as unknown as TemplateContext;
|
||||
const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings;
|
||||
@@ -83,7 +83,7 @@ function createMinimalRun(params?: {
|
||||
run: {
|
||||
sessionId: "session",
|
||||
sessionKey,
|
||||
surface: "whatsapp",
|
||||
messageProvider: "whatsapp",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
|
||||
@@ -186,9 +186,11 @@ export async function runReplyAgent(params: {
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: followupRun.run.sessionId,
|
||||
sessionKey,
|
||||
surface: sessionCtx.Surface?.trim().toLowerCase() || undefined,
|
||||
messageProvider:
|
||||
sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
sessionFile: followupRun.run.sessionFile,
|
||||
workspaceDir: followupRun.run.workspaceDir,
|
||||
agentDir: followupRun.run.agentDir,
|
||||
config: followupRun.run.config,
|
||||
skillsSnapshot: followupRun.run.skillsSnapshot,
|
||||
prompt: commandBody,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveTextChunkLimit, type TextChunkSurface } from "../chunk.js";
|
||||
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
|
||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||
|
||||
const BLOCK_CHUNK_SURFACES = new Set<TextChunkSurface>([
|
||||
const BLOCK_CHUNK_PROVIDERS = new Set<TextChunkProvider>([
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
@@ -14,24 +14,26 @@ const BLOCK_CHUNK_SURFACES = new Set<TextChunkSurface>([
|
||||
"webchat",
|
||||
]);
|
||||
|
||||
function normalizeChunkSurface(surface?: string): TextChunkSurface | undefined {
|
||||
if (!surface) return undefined;
|
||||
const cleaned = surface.trim().toLowerCase();
|
||||
return BLOCK_CHUNK_SURFACES.has(cleaned as TextChunkSurface)
|
||||
? (cleaned as TextChunkSurface)
|
||||
function normalizeChunkProvider(
|
||||
provider?: string,
|
||||
): TextChunkProvider | undefined {
|
||||
if (!provider) return undefined;
|
||||
const cleaned = provider.trim().toLowerCase();
|
||||
return BLOCK_CHUNK_PROVIDERS.has(cleaned as TextChunkProvider)
|
||||
? (cleaned as TextChunkProvider)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolveBlockStreamingChunking(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
surface?: string,
|
||||
provider?: string,
|
||||
): {
|
||||
minChars: number;
|
||||
maxChars: number;
|
||||
breakPreference: "paragraph" | "newline" | "sentence";
|
||||
} {
|
||||
const surfaceKey = normalizeChunkSurface(surface);
|
||||
const textLimit = resolveTextChunkLimit(cfg, surfaceKey);
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey);
|
||||
const chunkCfg = cfg?.agent?.blockStreamingChunk;
|
||||
const maxRequested = Math.max(
|
||||
1,
|
||||
|
||||
@@ -47,8 +47,8 @@ import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { incrementCompactionCount } from "./session-updates.js";
|
||||
|
||||
export type CommandContext = {
|
||||
surface: string;
|
||||
isWhatsAppSurface: boolean;
|
||||
provider: string;
|
||||
isWhatsAppProvider: boolean;
|
||||
ownerList: string[];
|
||||
isAuthorizedSender: boolean;
|
||||
senderE164?: string;
|
||||
@@ -123,7 +123,7 @@ export function buildCommandContext(params: {
|
||||
cfg,
|
||||
commandAuthorized: params.commandAuthorized,
|
||||
});
|
||||
const surface = (ctx.Surface ?? "").trim().toLowerCase();
|
||||
const provider = (ctx.Provider ?? "").trim().toLowerCase();
|
||||
const abortKey =
|
||||
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const rawBodyNormalized = triggerBodyNormalized;
|
||||
@@ -132,8 +132,8 @@ export function buildCommandContext(params: {
|
||||
: rawBodyNormalized;
|
||||
|
||||
return {
|
||||
surface,
|
||||
isWhatsAppSurface: auth.isWhatsAppSurface,
|
||||
provider,
|
||||
isWhatsAppProvider: auth.isWhatsAppProvider,
|
||||
ownerList: auth.ownerList,
|
||||
isAuthorizedSender: auth.isAuthorizedSender,
|
||||
senderE164: auth.senderE164,
|
||||
@@ -220,14 +220,14 @@ export async function handleCommands(params: {
|
||||
? normalizeE164(command.senderE164)
|
||||
: "";
|
||||
const isActivationOwner =
|
||||
!command.isWhatsAppSurface || activationOwnerList.length === 0
|
||||
!command.isWhatsAppProvider || activationOwnerList.length === 0
|
||||
? command.isAuthorizedSender
|
||||
: Boolean(activationSenderE164) &&
|
||||
activationOwnerList.includes(activationSenderE164);
|
||||
|
||||
if (
|
||||
!command.isAuthorizedSender ||
|
||||
(command.isWhatsAppSurface && !isActivationOwner)
|
||||
(command.isWhatsAppProvider && !isActivationOwner)
|
||||
) {
|
||||
logVerbose(
|
||||
`Ignoring /activation from unauthorized sender in group: ${command.senderE164 || "<unknown>"}`,
|
||||
@@ -402,7 +402,7 @@ export async function handleCommands(params: {
|
||||
const result = await compactEmbeddedPiSession({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
surface: command.surface,
|
||||
messageProvider: command.provider,
|
||||
sessionFile: resolveSessionTranscriptPath(sessionId),
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
@@ -469,7 +469,7 @@ export async function handleCommands(params: {
|
||||
cfg,
|
||||
entry: sessionEntry,
|
||||
sessionKey,
|
||||
surface: sessionEntry?.surface ?? command.surface,
|
||||
provider: sessionEntry?.provider ?? command.provider,
|
||||
chatType: sessionEntry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("createFollowupRunner compaction", () => {
|
||||
run: {
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
surface: "whatsapp",
|
||||
messageProvider: "whatsapp",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
|
||||
@@ -76,7 +76,7 @@ export function createFollowupRunner(params: {
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: queued.run.sessionId,
|
||||
sessionKey: queued.run.sessionKey,
|
||||
surface: queued.run.surface,
|
||||
messageProvider: queued.run.messageProvider,
|
||||
sessionFile: queued.run.sessionFile,
|
||||
workspaceDir: queued.run.workspaceDir,
|
||||
config: queued.run.config,
|
||||
|
||||
@@ -19,13 +19,13 @@ describe("resolveGroupRequireMention", () => {
|
||||
},
|
||||
};
|
||||
const ctx: TemplateContext = {
|
||||
Surface: "discord",
|
||||
Provider: "discord",
|
||||
From: "group:123",
|
||||
GroupRoom: "#general",
|
||||
GroupSpace: "145",
|
||||
};
|
||||
const groupResolution: GroupKeyResolution = {
|
||||
surface: "discord",
|
||||
provider: "discord",
|
||||
id: "123",
|
||||
chatType: "group",
|
||||
};
|
||||
@@ -44,12 +44,12 @@ describe("resolveGroupRequireMention", () => {
|
||||
},
|
||||
};
|
||||
const ctx: TemplateContext = {
|
||||
Surface: "slack",
|
||||
Provider: "slack",
|
||||
From: "slack:channel:C123",
|
||||
GroupSubject: "#general",
|
||||
};
|
||||
const groupResolution: GroupKeyResolution = {
|
||||
surface: "slack",
|
||||
provider: "slack",
|
||||
id: "C123",
|
||||
chatType: "group",
|
||||
};
|
||||
|
||||
@@ -50,22 +50,23 @@ export function resolveGroupRequireMention(params: {
|
||||
groupResolution?: GroupKeyResolution;
|
||||
}): boolean {
|
||||
const { cfg, ctx, groupResolution } = params;
|
||||
const surface = groupResolution?.surface ?? ctx.Surface?.trim().toLowerCase();
|
||||
const provider =
|
||||
groupResolution?.provider ?? ctx.Provider?.trim().toLowerCase();
|
||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
if (
|
||||
surface === "telegram" ||
|
||||
surface === "whatsapp" ||
|
||||
surface === "imessage"
|
||||
provider === "telegram" ||
|
||||
provider === "whatsapp" ||
|
||||
provider === "imessage"
|
||||
) {
|
||||
return resolveProviderGroupRequireMention({
|
||||
cfg,
|
||||
surface,
|
||||
provider,
|
||||
groupId,
|
||||
});
|
||||
}
|
||||
if (surface === "discord") {
|
||||
if (provider === "discord") {
|
||||
const guildEntry = resolveDiscordGuildEntry(
|
||||
cfg.discord?.guilds,
|
||||
groupSpace,
|
||||
@@ -90,7 +91,7 @@ export function resolveGroupRequireMention(params: {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (surface === "slack") {
|
||||
if (provider === "slack") {
|
||||
const channels = cfg.slack?.channels ?? {};
|
||||
const keys = Object.keys(channels);
|
||||
if (keys.length === 0) return true;
|
||||
@@ -137,18 +138,18 @@ export function buildGroupIntro(params: {
|
||||
params.defaultActivation;
|
||||
const subject = params.sessionCtx.GroupSubject?.trim();
|
||||
const members = params.sessionCtx.GroupMembers?.trim();
|
||||
const surface = params.sessionCtx.Surface?.trim().toLowerCase();
|
||||
const surfaceLabel = (() => {
|
||||
if (!surface) return "chat";
|
||||
if (surface === "whatsapp") return "WhatsApp";
|
||||
if (surface === "telegram") return "Telegram";
|
||||
if (surface === "discord") return "Discord";
|
||||
if (surface === "webchat") return "WebChat";
|
||||
return `${surface.at(0)?.toUpperCase() ?? ""}${surface.slice(1)}`;
|
||||
const provider = params.sessionCtx.Provider?.trim().toLowerCase();
|
||||
const providerLabel = (() => {
|
||||
if (!provider) return "chat";
|
||||
if (provider === "whatsapp") return "WhatsApp";
|
||||
if (provider === "telegram") return "Telegram";
|
||||
if (provider === "discord") return "Discord";
|
||||
if (provider === "webchat") return "WebChat";
|
||||
return `${provider.at(0)?.toUpperCase() ?? ""}${provider.slice(1)}`;
|
||||
})();
|
||||
const subjectLine = subject
|
||||
? `You are replying inside the ${surfaceLabel} group "${subject}".`
|
||||
: `You are replying inside a ${surfaceLabel} group chat.`;
|
||||
? `You are replying inside the ${providerLabel} group "${subject}".`
|
||||
: `You are replying inside a ${providerLabel} group chat.`;
|
||||
const membersLine = members ? `Group members: ${members}.` : undefined;
|
||||
const activationLine =
|
||||
activation === "always"
|
||||
|
||||
@@ -23,9 +23,11 @@ export type FollowupRun = {
|
||||
summaryLine?: string;
|
||||
enqueuedAt: number;
|
||||
run: {
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
surface?: string;
|
||||
messageProvider?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config: ClawdbotConfig;
|
||||
@@ -425,8 +427,8 @@ export function scheduleFollowupDrain(
|
||||
}
|
||||
})();
|
||||
}
|
||||
function defaultQueueModeForSurface(surface?: string): QueueMode {
|
||||
const normalized = surface?.trim().toLowerCase();
|
||||
function defaultQueueModeForProvider(provider?: string): QueueMode {
|
||||
const normalized = provider?.trim().toLowerCase();
|
||||
if (normalized === "discord") return "collect";
|
||||
if (normalized === "webchat") return "collect";
|
||||
if (normalized === "whatsapp") return "collect";
|
||||
@@ -437,23 +439,23 @@ function defaultQueueModeForSurface(surface?: string): QueueMode {
|
||||
}
|
||||
export function resolveQueueSettings(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
surface?: string;
|
||||
provider?: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
inlineMode?: QueueMode;
|
||||
inlineOptions?: Partial<QueueSettings>;
|
||||
}): QueueSettings {
|
||||
const surfaceKey = params.surface?.trim().toLowerCase();
|
||||
const providerKey = params.provider?.trim().toLowerCase();
|
||||
const queueCfg = params.cfg.routing?.queue;
|
||||
const surfaceModeRaw =
|
||||
surfaceKey && queueCfg?.bySurface
|
||||
? (queueCfg.bySurface as Record<string, string | undefined>)[surfaceKey]
|
||||
const providerModeRaw =
|
||||
providerKey && queueCfg?.byProvider
|
||||
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
|
||||
: undefined;
|
||||
const resolvedMode =
|
||||
params.inlineMode ??
|
||||
normalizeQueueMode(params.sessionEntry?.queueMode) ??
|
||||
normalizeQueueMode(surfaceModeRaw) ??
|
||||
normalizeQueueMode(providerModeRaw) ??
|
||||
normalizeQueueMode(queueCfg?.mode) ??
|
||||
defaultQueueModeForSurface(surfaceKey);
|
||||
defaultQueueModeForProvider(providerKey);
|
||||
const debounceRaw =
|
||||
params.inlineOptions?.debounceMs ??
|
||||
params.sessionEntry?.queueDebounceMs ??
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DEFAULT_RESET_TRIGGERS,
|
||||
type GroupKeyResolution,
|
||||
loadSessionStore,
|
||||
resolveAgentIdFromSessionKey,
|
||||
resolveGroupSessionKey,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
@@ -43,6 +44,7 @@ export async function initSessionState(params: {
|
||||
const { ctx, cfg, commandAuthorized } = params;
|
||||
const sessionCfg = cfg.session;
|
||||
const mainKey = sessionCfg?.mainKey ?? "main";
|
||||
const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey);
|
||||
const resetTriggers = sessionCfg?.resetTriggers?.length
|
||||
? sessionCfg.resetTriggers
|
||||
: DEFAULT_RESET_TRIGGERS;
|
||||
@@ -51,12 +53,12 @@ export async function initSessionState(params: {
|
||||
1,
|
||||
);
|
||||
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
||||
const storePath = resolveStorePath(sessionCfg?.store);
|
||||
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
|
||||
|
||||
const sessionStore: Record<string, SessionEntry> =
|
||||
loadSessionStore(storePath);
|
||||
let sessionKey: string | undefined;
|
||||
let sessionEntry: SessionEntry | undefined;
|
||||
let sessionEntry: SessionEntry;
|
||||
|
||||
let sessionId: string | undefined;
|
||||
let isNewSession = false;
|
||||
@@ -154,30 +156,30 @@ export async function initSessionState(params: {
|
||||
queueDrop: baseEntry?.queueDrop,
|
||||
displayName: baseEntry?.displayName,
|
||||
chatType: baseEntry?.chatType,
|
||||
surface: baseEntry?.surface,
|
||||
provider: baseEntry?.provider,
|
||||
subject: baseEntry?.subject,
|
||||
room: baseEntry?.room,
|
||||
space: baseEntry?.space,
|
||||
};
|
||||
if (groupResolution?.surface) {
|
||||
const surface = groupResolution.surface;
|
||||
if (groupResolution?.provider) {
|
||||
const provider = groupResolution.provider;
|
||||
const subject = ctx.GroupSubject?.trim();
|
||||
const space = ctx.GroupSpace?.trim();
|
||||
const explicitRoom = ctx.GroupRoom?.trim();
|
||||
const isRoomSurface = surface === "discord" || surface === "slack";
|
||||
const isRoomProvider = provider === "discord" || provider === "slack";
|
||||
const nextRoom =
|
||||
explicitRoom ??
|
||||
(isRoomSurface && subject && subject.startsWith("#")
|
||||
(isRoomProvider && subject && subject.startsWith("#")
|
||||
? subject
|
||||
: undefined);
|
||||
const nextSubject = nextRoom ? undefined : subject;
|
||||
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
||||
sessionEntry.surface = surface;
|
||||
sessionEntry.provider = provider;
|
||||
if (nextSubject) sessionEntry.subject = nextSubject;
|
||||
if (nextRoom) sessionEntry.room = nextRoom;
|
||||
if (space) sessionEntry.space = space;
|
||||
sessionEntry.displayName = buildGroupDisplayName({
|
||||
surface: sessionEntry.surface,
|
||||
provider: sessionEntry.provider,
|
||||
subject: sessionEntry.subject,
|
||||
room: sessionEntry.room,
|
||||
space: sessionEntry.space,
|
||||
|
||||
Reference in New Issue
Block a user