refactor: normalize hook agent providers

This commit is contained in:
Peter Steinberger
2026-01-09 17:40:36 +01:00
parent f146c9ef16
commit 51725a71a3
5 changed files with 53 additions and 36 deletions

View File

@@ -18,6 +18,7 @@
- CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured.
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
- Hooks: default hook agent delivery to true. (#533) — thanks @mcinteerj
- Hooks: normalize hook agent providers (aliases + msteams support).
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj

View File

@@ -71,7 +71,7 @@ Payload:
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging provider. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`. Defaults to `last`.
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack). Defaults to the last recipient in the main session.
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).

View File

@@ -87,6 +87,15 @@ describe("gateway hooks helpers", () => {
expect(imsg.value.provider).toBe("imessage");
}
const teams = normalizeAgentPayload(
{ message: "yo", provider: "teams" },
{ idFactory: () => "x" },
);
expect(teams.ok).toBe(true);
if (teams.ok) {
expect(teams.value.provider).toBe("msteams");
}
const bad = normalizeAgentPayload({ message: "yo", provider: "sms" });
expect(bad.ok).toBe(false);
});

View File

@@ -138,20 +138,41 @@ export type HookAgentPayload = {
wakeMode: "now" | "next-heartbeat";
sessionKey: string;
deliver: boolean;
provider:
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
provider: HookMessageProvider;
to?: string;
model?: string;
thinking?: string;
timeoutSeconds?: number;
};
const HOOK_PROVIDER_VALUES = [
"last",
"whatsapp",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"msteams",
] as const;
export type HookMessageProvider = (typeof HOOK_PROVIDER_VALUES)[number];
const hookProviderSet = new Set<string>(HOOK_PROVIDER_VALUES);
export const HOOK_PROVIDER_ERROR = `provider must be ${HOOK_PROVIDER_VALUES.join("|")}`;
export function resolveHookProvider(raw: unknown): HookMessageProvider | null {
if (raw === undefined) return "last";
if (typeof raw !== "string") return null;
const normalized = normalizeMessageProvider(raw);
if (!normalized || !hookProviderSet.has(normalized)) return null;
return normalized as HookMessageProvider;
}
export function resolveHookDeliver(raw: unknown): boolean {
return raw !== false;
}
export function normalizeAgentPayload(
payload: Record<string, unknown>,
opts?: { idFactory?: () => string },
@@ -175,30 +196,8 @@ export function normalizeAgentPayload(
typeof sessionKeyRaw === "string" && sessionKeyRaw.trim()
? sessionKeyRaw.trim()
: `hook:${idFactory()}`;
const providerRaw = payload.provider;
const providerNormalized =
typeof providerRaw === "string"
? normalizeMessageProvider(providerRaw)
: undefined;
const provider =
providerNormalized === "whatsapp" ||
providerNormalized === "telegram" ||
providerNormalized === "discord" ||
providerNormalized === "slack" ||
providerNormalized === "signal" ||
providerNormalized === "imessage" ||
providerNormalized === "last"
? providerNormalized
: providerRaw === undefined
? "last"
: null;
if (provider === null) {
return {
ok: false,
error:
"provider must be last|whatsapp|telegram|discord|slack|signal|imessage",
};
}
const provider = resolveHookProvider(payload.provider);
if (!provider) return { ok: false, error: HOOK_PROVIDER_ERROR };
const toRaw = payload.to;
const to =
typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
@@ -210,7 +209,7 @@ export function normalizeAgentPayload(
if (modelRaw !== undefined && !model) {
return { ok: false, error: "model required" };
}
const deliver = payload.deliver !== false;
const deliver = resolveHookDeliver(payload.deliver);
const thinkingRaw = payload.thinking;
const thinking =
typeof thinkingRaw === "string" && thinkingRaw.trim()

View File

@@ -11,11 +11,14 @@ import type { createSubsystemLogger } from "../logging.js";
import { handleControlUiHttpRequest } from "./control-ui.js";
import {
extractHookToken,
HOOK_PROVIDER_ERROR,
type HooksConfigResolved,
normalizeAgentPayload,
normalizeHookHeaders,
normalizeWakePayload,
readJsonBody,
resolveHookDeliver,
resolveHookProvider,
} from "./hooks.js";
import { applyHookMappings } from "./hooks-mapping.js";
@@ -171,13 +174,18 @@ export function createHooksRequestHandler(
sendJson(res, 200, { ok: true, mode: mapped.action.mode });
return true;
}
const provider = resolveHookProvider(mapped.action.provider);
if (!provider) {
sendJson(res, 400, { ok: false, error: HOOK_PROVIDER_ERROR });
return true;
}
const runId = dispatchAgentHook({
message: mapped.action.message,
name: mapped.action.name ?? "Hook",
wakeMode: mapped.action.wakeMode,
sessionKey: mapped.action.sessionKey ?? "",
deliver: mapped.action.deliver !== false,
provider: mapped.action.provider ?? "last",
deliver: resolveHookDeliver(mapped.action.deliver),
provider,
to: mapped.action.to,
model: mapped.action.model,
thinking: mapped.action.thinking,