fix(gateway): harden agent provider routing

This commit is contained in:
Peter Steinberger
2026-01-09 23:00:23 +01:00
parent 3adec35632
commit 79f5ccc99d
18 changed files with 327 additions and 89 deletions

View File

@@ -23,6 +23,7 @@
- Models: centralize model override validation + hooks Gmail warnings in doctor. (#602) — thanks @steipete
- Agents: avoid base-to-string error stringification in model fallback. (#604) — thanks @steipete
- Agents: `sessions_spawn` inherits the requester's provider for child runs (avoid WhatsApp fallback). (#528) — thanks @rlmestre
- Gateway/CLI: harden agent provider routing + validation (Slack/MS Teams + aliases). (follow-up #528) — thanks @steipete
- Agents: treat billing/insufficient-credits errors as failover-worthy so model fallbacks kick in. (#486) — thanks @steipete
- Auth: default billing disable backoff to 5h (doubling, 24h cap) and surface disabled/cooldown profiles in `models list` + doctor. (#486) — thanks @steipete
- Commands: harden slash command registry and list text-only commands in `/commands`.

2
src/agents/lanes.ts Normal file
View File

@@ -0,0 +1,2 @@
export const AGENT_LANE_NESTED = "nested" as const;
export const AGENT_LANE_SUBAGENT = "subagent" as const;

View File

@@ -8,6 +8,8 @@ import {
resolveStorePath,
} from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
import { AGENT_LANE_NESTED } from "./lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
import { isAnnounceSkip } from "./tools/sessions-send-helpers.js";
@@ -241,7 +243,8 @@ export async function runSubagentAnnounceFlow(params: {
message: "Sub-agent announce step.",
extraSystemPrompt: announcePrompt,
timeoutMs: params.timeoutMs,
lane: "nested",
provider: INTERNAL_MESSAGE_PROVIDER,
lane: AGENT_LANE_NESTED,
});
if (

View File

@@ -1,6 +1,8 @@
import crypto from "node:crypto";
import { callGateway } from "../../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
export async function readLatestAssistantReply(params: {
@@ -34,8 +36,8 @@ export async function runAgentStep(params: {
sessionKey: params.sessionKey,
idempotencyKey: stepIdem,
deliver: false,
provider: params.provider ?? "webchat",
lane: params.lane ?? "nested",
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER,
lane: params.lane ?? AGENT_LANE_NESTED,
extraSystemPrompt: params.extraSystemPrompt,
},
timeoutMs: 10_000,

View File

@@ -10,7 +10,11 @@ import {
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
import {
type GatewayMessageProvider,
INTERNAL_MESSAGE_PROVIDER,
} from "../../utils/message-provider.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringParam } from "./common.js";
@@ -297,8 +301,8 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey,
idempotencyKey,
deliver: false,
provider: "webchat",
lane: "nested",
provider: INTERNAL_MESSAGE_PROVIDER,
lane: AGENT_LANE_NESTED,
extraSystemPrompt: agentMessageContext,
};
const requesterSessionKey = opts?.agentSessionKey;
@@ -362,7 +366,7 @@ export function createSessionsSendTool(opts?: {
message: incomingMessage,
extraSystemPrompt: replyPrompt,
timeoutMs: announceTimeoutMs,
lane: "nested",
lane: AGENT_LANE_NESTED,
});
if (!replyText || isReplySkip(replyText)) {
break;
@@ -388,7 +392,7 @@ export function createSessionsSendTool(opts?: {
message: "Agent-to-agent announce step.",
extraSystemPrompt: announcePrompt,
timeoutMs: announceTimeoutMs,
lane: "nested",
lane: AGENT_LANE_NESTED,
});
if (
announceTarget &&

View File

@@ -11,6 +11,7 @@ import {
} from "../../routing/session-key.js";
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
import { resolveAgentConfig } from "../agent-scope.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
import { registerSubagentRun } from "../subagent-registry.js";
import type { AnyAgentTool } from "./common.js";
@@ -174,7 +175,7 @@ export function createSessionsSpawnTool(opts?: {
provider: opts?.agentProvider,
idempotencyKey: childIdem,
deliver: false,
lane: "subagent",
lane: AGENT_LANE_SUBAGENT,
extraSystemPrompt: childSystemPrompt,
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
label: label || undefined,

View File

@@ -24,6 +24,7 @@ import {
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "../imessage/accounts.js";
import { resolveMSTeamsCredentials } from "../msteams/token.js";
import {
type ChatProviderId,
getChatProviderMeta,
@@ -564,6 +565,20 @@ async function buildProviderStatusIndex(
});
}
{
const accountId = DEFAULT_ACCOUNT_ID;
const hasCreds = Boolean(resolveMSTeamsCredentials(cfg.msteams));
const hasConfig = Boolean(cfg.msteams);
const enabled = cfg.msteams?.enabled !== false;
map.set(providerAccountKey("msteams", accountId), {
provider: "msteams",
accountId,
state: hasCreds ? "configured" : "not configured",
enabled,
configured: hasCreds || hasConfig,
});
}
return map;
}
@@ -584,6 +599,8 @@ function resolveDefaultAccountId(
return resolveDefaultSignalAccountId(cfg) || DEFAULT_ACCOUNT_ID;
case "imessage":
return resolveDefaultIMessageAccountId(cfg) || DEFAULT_ACCOUNT_ID;
case "msteams":
return DEFAULT_ACCOUNT_ID;
}
}

View File

@@ -16,10 +16,12 @@ import {
formatUsageReportLines,
loadProviderUsageSummary,
} from "../../infra/provider-usage.js";
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
import {
type ChatProviderId,
listChatProviders,
} from "../../providers/registry.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import {
listSignalAccountIds,
@@ -104,6 +106,7 @@ export async function providersListCommand(
slack: listSlackAccountIds(cfg),
signal: listSignalAccountIds(cfg),
imessage: listIMessageAccountIds(cfg),
msteams: [DEFAULT_ACCOUNT_ID],
};
const lineBuilders: Record<
@@ -194,6 +197,19 @@ export async function providersListCommand(
});
return `- ${label}: ${formatEnabled(account.enabled)}`;
},
msteams: async (accountId) => {
const label = formatProviderAccountLabel({
provider: "msteams",
accountId,
providerStyle: theme.accent,
accountStyle: theme.heading,
});
const configured = Boolean(resolveMSTeamsCredentials(cfg.msteams));
const enabled = cfg.msteams?.enabled !== false;
return `- ${label}: ${formatConfigured(configured)}, ${formatEnabled(
enabled,
)}`;
},
};
const authStore = loadAuthProfileStore();
@@ -217,6 +233,7 @@ export async function providersListCommand(
slack: accountIdsByProvider.slack,
signal: accountIdsByProvider.signal,
imessage: accountIdsByProvider.imessage,
msteams: accountIdsByProvider.msteams,
},
auth: authProfiles,
...(usage ? { usage } : {}),

View File

@@ -42,6 +42,8 @@ function listAccountIds(cfg: ClawdbotConfig, provider: ChatProvider): string[] {
return listSignalAccountIds(cfg);
case "imessage":
return listIMessageAccountIds(cfg);
case "msteams":
return [DEFAULT_ACCOUNT_ID];
}
}

View File

@@ -14,7 +14,9 @@ import {
} from "../../imessage/accounts.js";
import { formatAge } from "../../infra/provider-summary.js";
import { collectProvidersStatusIssues } from "../../infra/providers-status-issues.js";
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
import { listChatProviders } from "../../providers/registry.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import {
listSignalAccountIds,
@@ -340,6 +342,18 @@ async function formatConfigProvidersStatusLines(
configured: imsgConfigured,
};
}),
msteams: [
{
accountId: DEFAULT_ACCOUNT_ID,
enabled: cfg.msteams?.enabled !== false,
configured: Boolean(resolveMSTeamsCredentials(cfg.msteams)),
dmPolicy: cfg.msteams?.dmPolicy ?? "pairing",
allowFrom: (cfg.msteams?.allowFrom ?? [])
.map((value) => String(value ?? "").trim())
.filter(Boolean)
.slice(0, 2),
},
],
} satisfies Partial<Record<ChatProvider, Array<Record<string, unknown>>>>;
// WhatsApp linked info (config-only best-effort).

View File

@@ -1,5 +1,6 @@
import { type Static, type TSchema, Type } from "@sinclair/typebox";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import { GATEWAY_AGENT_PROVIDER_VALUES } from "../../utils/message-provider.js";
const NonEmptyString = Type.String({ minLength: 1 });
const SessionLabelString = Type.String({
@@ -7,6 +8,10 @@ const SessionLabelString = Type.String({
maxLength: SESSION_LABEL_MAX_LENGTH,
});
const AgentProviderSchema = Type.Union(
GATEWAY_AGENT_PROVIDER_VALUES.map((provider) => Type.Literal(provider)),
);
export const PresenceEntrySchema = Type.Object(
{
host: Type.Optional(NonEmptyString),
@@ -225,7 +230,7 @@ export const AgentParamsSchema = Type.Object(
sessionKey: Type.Optional(Type.String()),
thinking: Type.Optional(Type.String()),
deliver: Type.Optional(Type.Boolean()),
provider: Type.Optional(Type.String()),
provider: Type.Optional(AgentProviderSchema),
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
lane: Type.Optional(Type.String()),
extraSystemPrompt: Type.Optional(Type.String()),

View File

@@ -12,7 +12,12 @@ import { registerAgentRunContext } from "../../infra/agent-events.js";
import { normalizeMainKey } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { normalizeMessageProvider } from "../../utils/message-provider.js";
import {
INTERNAL_MESSAGE_PROVIDER,
isDeliverableMessageProvider,
isGatewayMessageProvider,
normalizeMessageProvider,
} from "../../utils/message-provider.js";
import { normalizeE164 } from "../../utils.js";
import {
type AgentWaitParams,
@@ -161,21 +166,18 @@ export const agentHandlers: GatewayRequestHandlers = {
if (requestedProvider === "last") {
// WebChat is not a deliverable surface. Treat it as "unset" for routing,
// so VoiceWake and CLI callers don't get stuck with deliver=false.
if (lastProvider && lastProvider !== "webchat") return lastProvider;
return wantsDelivery ? "whatsapp" : "webchat";
if (lastProvider && lastProvider !== INTERNAL_MESSAGE_PROVIDER) {
return lastProvider;
}
return wantsDelivery ? "whatsapp" : INTERNAL_MESSAGE_PROVIDER;
}
if (
requestedProvider === "whatsapp" ||
requestedProvider === "telegram" ||
requestedProvider === "discord" ||
requestedProvider === "signal" ||
requestedProvider === "imessage" ||
requestedProvider === "webchat"
) {
return requestedProvider;
if (isGatewayMessageProvider(requestedProvider)) return requestedProvider;
if (lastProvider && lastProvider !== INTERNAL_MESSAGE_PROVIDER) {
return lastProvider;
}
if (lastProvider && lastProvider !== "webchat") return lastProvider;
return wantsDelivery ? "whatsapp" : "webchat";
return wantsDelivery ? "whatsapp" : INTERNAL_MESSAGE_PROVIDER;
})();
const resolvedTo = (() => {
@@ -184,13 +186,7 @@ export const agentHandlers: GatewayRequestHandlers = {
? request.to.trim()
: undefined;
if (explicit) return explicit;
if (
resolvedProvider === "whatsapp" ||
resolvedProvider === "telegram" ||
resolvedProvider === "discord" ||
resolvedProvider === "signal" ||
resolvedProvider === "imessage"
) {
if (isDeliverableMessageProvider(resolvedProvider)) {
return lastTo || undefined;
}
return undefined;
@@ -225,7 +221,9 @@ export const agentHandlers: GatewayRequestHandlers = {
return allowFrom[0];
})();
const deliver = request.deliver === true && resolvedProvider !== "webchat";
const deliver =
request.deliver === true &&
resolvedProvider !== INTERNAL_MESSAGE_PROVIDER;
const accepted = {
runId,

View File

@@ -286,6 +286,50 @@ describe("gateway server agent", () => {
await server.close();
});
test("agent routes main last-channel slack", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
testState.sessionStorePath = path.join(dir, "sessions.json");
await fs.writeFile(
testState.sessionStorePath,
JSON.stringify(
{
main: {
sessionId: "sess-slack",
updatedAt: Date.now(),
lastProvider: "slack",
lastTo: "channel:slack-123",
},
},
null,
2,
),
"utf-8",
);
const { server, ws } = await startServerWithClient();
await connectOk(ws);
const res = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
provider: "last",
deliver: true,
idempotencyKey: "idem-agent-last-slack",
});
expect(res.ok).toBe(true);
const spy = vi.mocked(agentCommand);
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
expectProviders(call, "slack");
expect(call.to).toBe("channel:slack-123");
expect(call.deliver).toBe(true);
expect(call.bestEffortDeliver).toBe(true);
expect(call.sessionId).toBe("sess-slack");
ws.close();
await server.close();
});
test("agent routes main last-channel signal", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
testState.sessionStorePath = path.join(dir, "sessions.json");
@@ -330,6 +374,125 @@ describe("gateway server agent", () => {
await server.close();
});
test("agent routes main last-channel msteams", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
testState.sessionStorePath = path.join(dir, "sessions.json");
await fs.writeFile(
testState.sessionStorePath,
JSON.stringify(
{
main: {
sessionId: "sess-teams",
updatedAt: Date.now(),
lastProvider: "msteams",
lastTo: "conversation:teams-123",
},
},
null,
2,
),
"utf-8",
);
const { server, ws } = await startServerWithClient();
await connectOk(ws);
const res = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
provider: "last",
deliver: true,
idempotencyKey: "idem-agent-last-msteams",
});
expect(res.ok).toBe(true);
const spy = vi.mocked(agentCommand);
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
expectProviders(call, "msteams");
expect(call.to).toBe("conversation:teams-123");
expect(call.deliver).toBe(true);
expect(call.bestEffortDeliver).toBe(true);
expect(call.sessionId).toBe("sess-teams");
ws.close();
await server.close();
});
test("agent accepts provider aliases (imsg/teams)", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
testState.sessionStorePath = path.join(dir, "sessions.json");
await fs.writeFile(
testState.sessionStorePath,
JSON.stringify(
{
main: {
sessionId: "sess-alias",
updatedAt: Date.now(),
lastProvider: "imessage",
lastTo: "chat_id:123",
},
},
null,
2,
),
"utf-8",
);
const { server, ws } = await startServerWithClient();
await connectOk(ws);
const resIMessage = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
provider: "imsg",
deliver: true,
idempotencyKey: "idem-agent-imsg",
});
expect(resIMessage.ok).toBe(true);
const resTeams = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
provider: "teams",
to: "conversation:teams-abc",
deliver: false,
idempotencyKey: "idem-agent-teams",
});
expect(resTeams.ok).toBe(true);
const spy = vi.mocked(agentCommand);
const lastIMessageCall = spy.mock.calls.at(-2)?.[0] as Record<
string,
unknown
>;
expectProviders(lastIMessageCall, "imessage");
expect(lastIMessageCall.to).toBe("chat_id:123");
const lastTeamsCall = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
expectProviders(lastTeamsCall, "msteams");
expect(lastTeamsCall.to).toBe("conversation:teams-abc");
ws.close();
await server.close();
});
test("agent rejects unknown provider", async () => {
const { server, ws } = await startServerWithClient();
await connectOk(ws);
const res = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
provider: "sms",
idempotencyKey: "idem-agent-bad-provider",
});
expect(res.ok).toBe(false);
expect(res.error?.code).toBe("INVALID_REQUEST");
ws.close();
await server.close();
});
test("agent ignores webchat last-channel for routing", async () => {
testState.allowFrom = ["+1555"];
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));

View File

@@ -5,34 +5,23 @@ import { resolveMSTeamsCredentials } from "../../msteams/token.js";
import { listEnabledSignalAccounts } from "../../signal/accounts.js";
import { listEnabledSlackAccounts } from "../../slack/accounts.js";
import { listEnabledTelegramAccounts } from "../../telegram/accounts.js";
import { normalizeMessageProvider } from "../../utils/message-provider.js";
import {
DELIVERABLE_MESSAGE_PROVIDERS,
type DeliverableMessageProvider,
normalizeMessageProvider,
} from "../../utils/message-provider.js";
import {
listEnabledWhatsAppAccounts,
resolveWhatsAppAccount,
} from "../../web/accounts.js";
import { webAuthExists } from "../../web/session.js";
export type MessageProviderId =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "msteams";
export type MessageProviderId = DeliverableMessageProvider;
const MESSAGE_PROVIDERS: MessageProviderId[] = [
"whatsapp",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"msteams",
];
const MESSAGE_PROVIDERS = [...DELIVERABLE_MESSAGE_PROVIDERS];
function isKnownProvider(value: string): value is MessageProviderId {
return (MESSAGE_PROVIDERS as string[]).includes(value);
return (MESSAGE_PROVIDERS as readonly string[]).includes(value);
}
async function isWhatsAppConfigured(cfg: ClawdbotConfig): Promise<boolean> {

View File

@@ -1,16 +1,12 @@
import type { ClawdbotConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import type {
DeliverableMessageProvider,
GatewayMessageProvider,
} from "../../utils/message-provider.js";
import { normalizeE164 } from "../../utils.js";
export type OutboundProvider =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "msteams"
| "none";
export type OutboundProvider = DeliverableMessageProvider | "none";
export type HeartbeatTarget = OutboundProvider | "last";
@@ -25,15 +21,7 @@ export type OutboundTargetResolution =
| { ok: false; error: Error };
export function resolveOutboundTarget(params: {
provider:
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "msteams"
| "webchat";
provider: GatewayMessageProvider;
to?: string;
allowFrom?: string[];
}): OutboundTargetResolution {

View File

@@ -9,6 +9,7 @@ import {
describe("provider registry", () => {
it("normalizes aliases", () => {
expect(normalizeChatProviderId("imsg")).toBe("imessage");
expect(normalizeChatProviderId("teams")).toBe("msteams");
});
it("keeps Telegram first in the default order", () => {

View File

@@ -1,3 +1,5 @@
import { normalizeMessageProvider } from "../utils/message-provider.js";
export const CHAT_PROVIDER_ORDER = [
"telegram",
"whatsapp",
@@ -5,6 +7,7 @@ export const CHAT_PROVIDER_ORDER = [
"slack",
"signal",
"imessage",
"msteams",
] as const;
export type ChatProviderId = (typeof CHAT_PROVIDER_ORDER)[number];
@@ -69,10 +72,14 @@ const CHAT_PROVIDER_META: Record<ChatProviderId, ChatProviderMeta> = {
docsLabel: "imessage",
blurb: "this is still a work in progress.",
},
};
const CHAT_PROVIDER_ALIASES: Record<string, ChatProviderId> = {
imsg: "imessage",
msteams: {
id: "msteams",
label: "MS Teams",
selectionLabel: "Microsoft Teams (Bot Framework)",
docsPath: "/msteams",
docsLabel: "msteams",
blurb: "supported (Bot Framework).",
},
};
const WEBSITE_URL = "https://clawd.bot";
@@ -88,9 +95,8 @@ export function getChatProviderMeta(id: ChatProviderId): ChatProviderMeta {
export function normalizeChatProviderId(
raw?: string | null,
): ChatProviderId | null {
const trimmed = (raw ?? "").trim().toLowerCase();
if (!trimmed) return null;
const normalized = CHAT_PROVIDER_ALIASES[trimmed] ?? trimmed;
const normalized = normalizeMessageProvider(raw);
if (!normalized) return null;
return CHAT_PROVIDER_ORDER.includes(normalized as ChatProviderId)
? (normalized as ChatProviderId)
: null;

View File

@@ -8,17 +8,7 @@ export function normalizeMessageProvider(
return normalized;
}
export type GatewayMessageProvider =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "msteams"
| "webchat";
const GATEWAY_MESSAGE_PROVIDERS: GatewayMessageProvider[] = [
export const DELIVERABLE_MESSAGE_PROVIDERS = [
"whatsapp",
"telegram",
"discord",
@@ -26,13 +16,48 @@ const GATEWAY_MESSAGE_PROVIDERS: GatewayMessageProvider[] = [
"signal",
"imessage",
"msteams",
] as const;
export type DeliverableMessageProvider =
(typeof DELIVERABLE_MESSAGE_PROVIDERS)[number];
export const INTERNAL_MESSAGE_PROVIDER = "webchat" as const;
export type InternalMessageProvider = typeof INTERNAL_MESSAGE_PROVIDER;
export type GatewayMessageProvider =
| DeliverableMessageProvider
| InternalMessageProvider;
export const GATEWAY_MESSAGE_PROVIDERS = [
...DELIVERABLE_MESSAGE_PROVIDERS,
"webchat",
];
] as const;
export const GATEWAY_AGENT_PROVIDER_ALIASES = ["imsg", "teams"] as const;
export type GatewayAgentProviderAlias =
(typeof GATEWAY_AGENT_PROVIDER_ALIASES)[number];
export type GatewayAgentProviderHint =
| GatewayMessageProvider
| "last"
| GatewayAgentProviderAlias;
export const GATEWAY_AGENT_PROVIDER_VALUES = [
...GATEWAY_MESSAGE_PROVIDERS,
"last",
...GATEWAY_AGENT_PROVIDER_ALIASES,
] as const;
export function isGatewayMessageProvider(
value: string,
): value is GatewayMessageProvider {
return (GATEWAY_MESSAGE_PROVIDERS as string[]).includes(value);
return (GATEWAY_MESSAGE_PROVIDERS as readonly string[]).includes(value);
}
export function isDeliverableMessageProvider(
value: string,
): value is DeliverableMessageProvider {
return (DELIVERABLE_MESSAGE_PROVIDERS as readonly string[]).includes(value);
}
export function resolveGatewayMessageProvider(