Move provider to a plugin-architecture (#661)

* refactor: introduce provider plugin registry

* refactor: move provider CLI to plugins

* docs: add provider plugin implementation notes

* refactor: shift provider runtime logic into plugins

* refactor: add plugin defaults and summaries

* docs: update provider plugin notes

* feat(commands): add /commands slash list

* Auto-reply: tidy help message

* Auto-reply: fix status command lint

* Tests: align google shared expectations

* Auto-reply: tidy help message

* Auto-reply: fix status command lint

* refactor: move provider routing into plugins

* test: align agent routing expectations

* docs: update provider plugin notes

* refactor: route replies via provider plugins

* docs: note route-reply plugin hooks

* refactor: extend provider plugin contract

* refactor: derive provider status from plugins

* refactor: unify gateway provider control

* refactor: use plugin metadata in auto-reply

* fix: parenthesize cron target selection

* refactor: derive gateway methods from plugins

* refactor: generalize provider logout

* refactor: route provider logout through plugins

* refactor: move WhatsApp web login methods into plugin

* refactor: generalize provider log prefixes

* refactor: centralize default chat provider

* refactor: derive provider lists from registry

* refactor: move provider reload noops into plugins

* refactor: resolve web login provider via alias

* refactor: derive CLI provider options from plugins

* refactor: derive prompt provider list from plugins

* style: apply biome lint fixes

* fix: resolve provider routing edge cases

* docs: update provider plugin refactor notes

* fix(gateway): harden agent provider routing

* refactor: move provider routing into plugins

* refactor: move provider CLI to plugins

* refactor: derive provider lists from registry

* fix: restore slash command parsing

* refactor: align provider ids for schema

* refactor: unify outbound target resolution

* fix: keep outbound labels stable

* feat: add msteams to cron surfaces

* fix: clean up lint build issues

* refactor: localize chat provider alias normalization

* refactor: drive gateway provider lists from plugins

* docs: update provider plugin notes

* style: format message-provider

* fix: avoid provider registry init cycles

* style: sort message-provider imports

* fix: relax provider alias map typing

* refactor: move provider routing into plugins

* refactor: add plugin pairing/config adapters

* refactor: route pairing and provider removal via plugins

* refactor: align auto-reply provider typing

* test: stabilize telegram media mocks

* docs: update provider plugin refactor notes

* refactor: pluginize outbound targets

* refactor: pluginize provider selection

* refactor: generalize text chunk limits

* docs: update provider plugin notes

* refactor: generalize group session/config

* fix: normalize provider id for room detection

* fix: avoid provider init in system prompt

* style: formatting cleanup

* refactor: normalize agent delivery targets

* test: update outbound delivery labels

* chore: fix lint regressions

* refactor: extend provider plugin adapters

* refactor: move elevated/block streaming defaults to plugins

* refactor: defer outbound send deps to plugins

* docs: note plugin-driven streaming/elevated defaults

* refactor: centralize webchat provider constant

* refactor: add provider setup adapters

* refactor: delegate provider add config to plugins

* docs: document plugin-driven provider add

* refactor: add plugin state/binding metadata

* refactor: build agent provider status from plugins

* docs: note plugin-driven agent bindings

* refactor: centralize internal provider constant usage

* fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing)

* refactor: centralize default chat provider

* refactor: centralize WhatsApp target normalization

* refactor: move provider routing into plugins

* refactor: normalize agent delivery targets

* chore: fix lint regressions

* fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing)

* feat: expand provider plugin adapters

* refactor: route auto-reply via provider plugins

* fix: align WhatsApp target normalization

* fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing)

* refactor: centralize WhatsApp target normalization

* feat: add /config chat config updates

* docs: add /config get alias

* feat(commands): add /commands slash list

* refactor: centralize default chat provider

* style: apply biome lint fixes

* chore: fix lint regressions

* fix: clean up whatsapp allowlist typing

* style: format config command helpers

* refactor: pluginize tool threading context

* refactor: normalize session announce targets

* docs: note new plugin threading and announce hooks

* refactor: pluginize message actions

* docs: update provider plugin actions notes

* fix: align provider action adapters

* refactor: centralize webchat checks

* style: format message provider helpers

* refactor: move provider onboarding into adapters

* docs: note onboarding provider adapters

* feat: add msteams onboarding adapter

* style: organize onboarding imports

* fix: normalize msteams allowFrom types

* feat: add plugin text chunk limits

* refactor: use plugin chunk limit fallbacks

* feat: add provider mention stripping hooks

* style: organize provider plugin type imports

* refactor: generalize health snapshots

* refactor: update macOS health snapshot handling

* docs: refresh health snapshot notes

* style: format health snapshot updates

* refactor: drive security warnings via plugins

* docs: note provider security adapter

* style: format provider security adapters

* refactor: centralize provider account defaults

* refactor: type gateway client identity constants

* chore: regen gateway protocol swift

* fix: degrade health on failed provider probe

* refactor: centralize pairing approve hint

* docs: add plugin CLI command references

* refactor: route auth and tool sends through plugins

* docs: expand provider plugin hooks

* refactor: document provider docking touchpoints

* refactor: normalize internal provider defaults

* refactor: streamline outbound delivery wiring

* refactor: make provider onboarding plugin-owned

* refactor: support provider-owned agent tools

* refactor: move telegram draft chunking into telegram module

* refactor: infer provider tool sends via extractToolSend

* fix: repair plugin onboarding imports

* refactor: de-dup outbound target normalization

* style: tidy plugin and agent imports

* refactor: data-drive provider selection line

* fix: satisfy lint after provider plugin rebase

* test: deflake gateway-cli coverage

* style: format gateway-cli coverage test

* refactor(provider-plugins): simplify provider ids

* test(pairing-cli): avoid provider-specific ternary

* style(macos): swiftformat HealthStore

* refactor(sandbox): derive provider tool denylist

* fix(sandbox): avoid plugin init in defaults

* refactor(provider-plugins): centralize provider aliases

* style(test): satisfy biome

* refactor(protocol): v3 providers.status maps

* refactor(ui): adapt to protocol v3

* refactor(macos): adapt to protocol v3

* test: update providers.status v3 fixtures

* refactor(gateway): map provider runtime snapshot

* test(gateway): update reload runtime snapshot

* refactor(whatsapp): normalize heartbeat provider id

* docs(refactor): update provider plugin notes

* style: satisfy biome after rebase

* fix: describe sandboxed elevated in prompt

* feat(gateway): add agent image attachments + live probe

* refactor: derive CLI provider options from plugins

* fix(gateway): harden agent provider routing

* fix(gateway): harden agent provider routing

* refactor: align provider ids for schema

* fix(protocol): keep agent provider string

* fix(gateway): harden agent provider routing

* fix(protocol): keep agent provider string

* refactor: normalize agent delivery targets

* refactor: support provider-owned agent tools

* refactor(config): provider-keyed elevated allowFrom

* style: satisfy biome

* fix(gateway): appease provider narrowing

* style: satisfy biome

* refactor(reply): move group intro hints into plugin

* fix(reply): avoid plugin registry init cycle

* refactor(providers): add lightweight provider dock

* refactor(gateway): use typed client id in connect

* refactor(providers): document docks and avoid init cycles

* refactor(providers): make media limit helper generic

* fix(providers): break plugin registry import cycles

* style: satisfy biome

* refactor(status-all): build providers table from plugins

* refactor(gateway): delegate web login to provider plugin

* refactor(provider): drop web alias

* refactor(provider): lazy-load monitors

* style: satisfy lint/format

* style: format status-all providers table

* style: swiftformat gateway discovery model

* test: make reload plan plugin-driven

* fix: avoid token stringification in status-all

* refactor: make provider IDs explicit in status

* feat: warn on signal/imessage provider runtime errors

* test: cover gateway provider runtime warnings in status

* fix: add runtime kind to provider status issues

* test: cover health degradation on probe failure

* fix: keep routeReply lightweight

* style: organize routeReply imports

* refactor(web): extract auth-store helpers

* refactor(whatsapp): lazy login imports

* refactor(outbound): route replies via plugin outbound

* docs: update provider plugin notes

* style: format provider status issues

* fix: make sandbox scope warning wrap-safe

* refactor: load outbound adapters from provider plugins

* docs: update provider plugin outbound notes

* style(macos): fix swiftformat lint

* docs: changelog for provider plugins

* fix(macos): satisfy swiftformat

* fix(macos): open settings via menu action

* style: format after rebase

* fix(macos): open Settings via menu action

---------

Co-authored-by: LK <luke@kyohere.com>
Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com>
Co-authored-by: Xin <xin@imfing.com>
This commit is contained in:
Peter Steinberger
2026-01-11 11:45:25 +00:00
committed by GitHub
parent 23eec7d841
commit 7acd26a2fc
232 changed files with 13642 additions and 10809 deletions

View File

@@ -1 +1,3 @@
// Backwards-compatible entry point.
// Implementation lives in `src/agents/cli-runner.ts` (so we can reuse the same runner for other CLIs).
export { runClaudeCliAgent, runCliAgent } from "./cli-runner.js";

View File

@@ -1,3 +1,8 @@
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
export type MessagingToolSend = {
tool: string;
provider: string;
@@ -5,101 +10,29 @@ export type MessagingToolSend = {
to?: string;
};
const MESSAGING_TOOLS = new Set([
"telegram",
"whatsapp",
"discord",
"slack",
"sessions_send",
"message",
]);
const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
// Provider docking: any plugin with `actions` opts into messaging tool handling.
export function isMessagingTool(toolName: string): boolean {
return MESSAGING_TOOLS.has(toolName);
if (CORE_MESSAGING_TOOLS.has(toolName)) return true;
const providerId = normalizeProviderId(toolName);
return Boolean(providerId && getProviderPlugin(providerId)?.actions);
}
export function isMessagingToolSendAction(
toolName: string,
actionRaw: string,
args: Record<string, unknown>,
): boolean {
const action = actionRaw.trim();
const action = typeof args.action === "string" ? args.action.trim() : "";
if (toolName === "sessions_send") return true;
if (toolName === "message") {
return action === "send" || action === "thread-reply";
}
return action === "sendMessage" || action === "threadReply";
}
function normalizeSlackTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
if (mentionMatch) return `user:${mentionMatch[1]}`.toLowerCase();
if (trimmed.startsWith("user:")) {
const id = trimmed.slice(5).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("channel:")) {
const id = trimmed.slice(8).trim();
return id ? `channel:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("slack:")) {
const id = trimmed.slice(6).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("@")) {
const id = trimmed.slice(1).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("#")) {
const id = trimmed.slice(1).trim();
return id ? `channel:${id}`.toLowerCase() : undefined;
}
return `channel:${trimmed}`.toLowerCase();
}
function normalizeDiscordTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
if (mentionMatch) return `user:${mentionMatch[1]}`.toLowerCase();
if (trimmed.startsWith("user:")) {
const id = trimmed.slice(5).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("channel:")) {
const id = trimmed.slice(8).trim();
return id ? `channel:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("discord:")) {
const id = trimmed.slice(8).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
if (trimmed.startsWith("@")) {
const id = trimmed.slice(1).trim();
return id ? `user:${id}`.toLowerCase() : undefined;
}
return `channel:${trimmed}`.toLowerCase();
}
function normalizeTelegramTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
let normalized = trimmed;
if (normalized.startsWith("telegram:")) {
normalized = normalized.slice("telegram:".length).trim();
} else if (normalized.startsWith("tg:")) {
normalized = normalized.slice("tg:".length).trim();
} else if (normalized.startsWith("group:")) {
normalized = normalized.slice("group:".length).trim();
}
if (!normalized) return undefined;
const tmeMatch =
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;
if (!normalized) return undefined;
return `telegram:${normalized}`.toLowerCase();
const providerId = normalizeProviderId(toolName);
if (!providerId) return false;
const plugin = getProviderPlugin(providerId);
if (!plugin?.actions?.extractToolSend) return false;
return Boolean(plugin.actions.extractToolSend({ args })?.to);
}
export function normalizeTargetForProvider(
@@ -107,14 +40,10 @@ export function normalizeTargetForProvider(
raw?: string,
): string | undefined {
if (!raw) return undefined;
switch (provider.trim().toLowerCase()) {
case "slack":
return normalizeSlackTarget(raw);
case "discord":
return normalizeDiscordTarget(raw);
case "telegram":
return normalizeTelegramTarget(raw);
default:
return raw.trim().toLowerCase() || undefined;
}
const providerId = normalizeProviderId(provider);
const plugin = providerId ? getProviderPlugin(providerId) : undefined;
const normalized =
plugin?.messaging?.normalizeTarget?.(raw) ??
(raw.trim().toLowerCase() || undefined);
return normalized || undefined;
}

View File

@@ -117,6 +117,7 @@ import {
type SkillSnapshot,
} from "./skills.js";
import { buildAgentSystemPrompt } from "./system-prompt.js";
import { buildToolSummaryMap } from "./tool-summaries.js";
import { normalizeUsage, type UsageLike } from "./usage.js";
import {
filterBootstrapFilesForSession,
@@ -644,6 +645,7 @@ function buildEmbeddedSystemPrompt(params: {
runtimeInfo: params.runtimeInfo,
sandboxInfo: params.sandboxInfo,
toolNames: params.tools.map((tool) => tool.name),
toolSummaries: buildToolSummaryMap(params.tools),
modelAliasLines: params.modelAliasLines,
userTimezone: params.userTimezone,
userTime: params.userTime,

View File

@@ -9,6 +9,10 @@ import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import { resolveStateDir } from "../config/paths.js";
import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
import { truncateUtf16Safe } from "../utils.js";
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
@@ -142,59 +146,37 @@ function extractMessagingToolSend(
toolName: string,
args: Record<string, unknown>,
): MessagingToolSend | undefined {
// Provider docking: new provider tools must implement plugin.actions.extractToolSend.
const action = typeof args.action === "string" ? args.action.trim() : "";
const accountIdRaw =
typeof args.accountId === "string" ? args.accountId.trim() : undefined;
const accountId = accountIdRaw ? accountIdRaw : undefined;
if (toolName === "slack") {
if (action !== "sendMessage") return undefined;
const toRaw = typeof args.to === "string" ? args.to : undefined;
if (!toRaw) return undefined;
const to = normalizeTargetForProvider("slack", toRaw);
return to
? { tool: toolName, provider: "slack", accountId, to }
: undefined;
}
if (toolName === "discord") {
if (action === "sendMessage") {
const toRaw = typeof args.to === "string" ? args.to : undefined;
if (!toRaw) return undefined;
const to = normalizeTargetForProvider("discord", toRaw);
return to
? { tool: toolName, provider: "discord", accountId, to }
: undefined;
}
if (action === "threadReply") {
const channelId =
typeof args.channelId === "string" ? args.channelId.trim() : "";
if (!channelId) return undefined;
const to = normalizeTargetForProvider("discord", `channel:${channelId}`);
return to
? { tool: toolName, provider: "discord", accountId, to }
: undefined;
}
return undefined;
}
if (toolName === "telegram") {
if (action !== "sendMessage") return undefined;
const toRaw = typeof args.to === "string" ? args.to : undefined;
if (!toRaw) return undefined;
const to = normalizeTargetForProvider("telegram", toRaw);
return to
? { tool: toolName, provider: "telegram", accountId, to }
: undefined;
}
if (toolName === "message") {
if (action !== "send" && action !== "thread-reply") return undefined;
const toRaw = typeof args.to === "string" ? args.to : undefined;
if (!toRaw) return undefined;
const providerRaw =
typeof args.provider === "string" ? args.provider.trim() : "";
const provider = providerRaw ? providerRaw.toLowerCase() : "message";
const providerId = providerRaw ? normalizeProviderId(providerRaw) : null;
const provider =
providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message");
const to = normalizeTargetForProvider(provider, toRaw);
return to ? { tool: toolName, provider, accountId, to } : undefined;
}
return undefined;
const providerId = normalizeProviderId(toolName);
if (!providerId) return undefined;
const plugin = getProviderPlugin(providerId);
const extracted = plugin?.actions?.extractToolSend?.({ args });
if (!extracted?.to) return undefined;
const to = normalizeTargetForProvider(providerId, extracted.to);
return to
? {
tool: toolName,
provider: providerId,
accountId: extracted.accountId ?? accountId,
to,
}
: undefined;
}
export function subscribeEmbeddedPiSession(params: {
@@ -564,7 +546,10 @@ export function subscribeEmbeddedPiSession(params: {
typeof argsRecord.action === "string"
? argsRecord.action.trim()
: "";
const isMessagingSend = isMessagingToolSendAction(toolName, action);
const isMessagingSend = isMessagingToolSendAction(
toolName,
argsRecord,
);
if (isMessagingSend) {
const sendTarget = extractMessagingToolSend(toolName, argsRecord);
if (sendTarget) {

View File

@@ -6,12 +6,10 @@ import {
createWriteTool,
readTool,
} from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../config/config.js";
import { detectMime } from "../media/mime.js";
import { isSubagentSessionKey } from "../routing/session-key.js";
import { resolveGatewayMessageProvider } from "../utils/message-provider.js";
import { startWebLoginWithQr, waitForWebLogin } from "../web/login-qr.js";
import {
resolveAgentConfig,
resolveAgentIdFromSessionKey,
@@ -24,6 +22,7 @@ import {
} from "./bash-tools.js";
import { createClawdbotTools } from "./clawdbot-tools.js";
import type { ModelAuthMode } from "./model-auth.js";
import { listProviderAgentTools } from "./provider-tools.js";
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
import { assertSandboxPath } from "./sandbox-paths.js";
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
@@ -400,75 +399,6 @@ function createSandboxedEditTool(root: string) {
return wrapSandboxPathGuard(base as unknown as AnyAgentTool, root);
}
function createWhatsAppLoginTool(): AnyAgentTool {
return {
label: "WhatsApp Login",
name: "whatsapp_login",
description:
"Generate a WhatsApp QR code for linking, or wait for the scan to complete.",
// NOTE: Using Type.Unsafe for action enum instead of Type.Union([Type.Literal(...)])
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
parameters: Type.Object({
action: Type.Unsafe<"start" | "wait">({
type: "string",
enum: ["start", "wait"],
}),
timeoutMs: Type.Optional(Type.Number()),
force: Type.Optional(Type.Boolean()),
}),
execute: async (_toolCallId, args) => {
const action = (args as { action?: string })?.action ?? "start";
if (action === "wait") {
const result = await waitForWebLogin({
timeoutMs:
typeof (args as { timeoutMs?: unknown }).timeoutMs === "number"
? (args as { timeoutMs?: number }).timeoutMs
: undefined,
});
return {
content: [{ type: "text", text: result.message }],
details: { connected: result.connected },
};
}
const result = await startWebLoginWithQr({
timeoutMs:
typeof (args as { timeoutMs?: unknown }).timeoutMs === "number"
? (args as { timeoutMs?: number }).timeoutMs
: undefined,
force:
typeof (args as { force?: unknown }).force === "boolean"
? (args as { force?: boolean }).force
: false,
});
if (!result.qrDataUrl) {
return {
content: [
{
type: "text",
text: result.message,
},
],
details: { qr: false },
};
}
const text = [
result.message,
"",
"Open WhatsApp → Linked Devices and scan:",
"",
`![whatsapp-qr](${result.qrDataUrl})`,
].join("\n");
return {
content: [{ type: "text", text }],
details: { qr: true },
};
},
};
}
function createClawdbotReadTool(base: AnyAgentTool): AnyAgentTool {
return {
...base,
@@ -635,7 +565,8 @@ export function createClawdbotCodingTools(options?: {
: []),
bashTool as unknown as AnyAgentTool,
processTool as unknown as AnyAgentTool,
createWhatsAppLoginTool(),
// Provider docking: include provider-defined agent tools (login, etc.).
...listProviderAgentTools({ cfg: options?.config }),
...createClawdbotTools({
browserControlUrl: sandbox?.browser?.controlUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,

View File

@@ -0,0 +1,17 @@
import type { ClawdbotConfig } from "../config/config.js";
import { listProviderPlugins } from "../providers/plugins/index.js";
import type { ProviderAgentTool } from "../providers/plugins/types.js";
export function listProviderAgentTools(params: {
cfg?: ClawdbotConfig;
}): ProviderAgentTool[] {
// Provider docking: aggregate provider-owned tools (login, etc.).
const tools: ProviderAgentTool[] = [];
for (const plugin of listProviderPlugins()) {
const entry = plugin.agentTools;
if (!entry) continue;
const resolved = typeof entry === "function" ? entry(params) : entry;
if (Array.isArray(resolved)) tools.push(...resolved);
}
return tools;
}

View File

@@ -19,6 +19,7 @@ import {
loadConfig,
STATE_DIR_CLAWDBOT,
} from "../config/config.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import {
buildAgentMainSessionKey,
normalizeAgentId,
@@ -176,13 +177,14 @@ const DEFAULT_TOOL_ALLOW = [
"sessions_spawn",
"session_status",
];
// Provider docking: keep sandbox policy aligned with provider tool names.
const DEFAULT_TOOL_DENY = [
"browser",
"canvas",
"nodes",
"cron",
"discord",
"gateway",
...PROVIDER_IDS,
];
export const DEFAULT_SANDBOX_BROWSER_IMAGE =
"clawdbot-sandbox-browser:bookworm-slim";

View File

@@ -1,7 +1,10 @@
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
const MESSAGE_PROVIDER_OPTIONS = PROVIDER_IDS.join("|");
export function buildAgentSystemPrompt(params: {
workspaceDir: string;
defaultThinkLevel?: ThinkLevel;
@@ -10,6 +13,7 @@ export function buildAgentSystemPrompt(params: {
ownerNumbers?: string[];
reasoningTagHint?: boolean;
toolNames?: string[];
toolSummaries?: Record<string, string>;
modelAliasLines?: string[];
userTimezone?: string;
userTime?: string;
@@ -42,7 +46,7 @@ export function buildAgentSystemPrompt(params: {
};
};
}) {
const toolSummaries: Record<string, string> = {
const coreToolSummaries: Record<string, string> = {
read: "Read file contents",
write: "Create or overwrite files",
edit: "Make precise edits to files",
@@ -51,7 +55,7 @@ export function buildAgentSystemPrompt(params: {
ls: "List directory contents",
bash: "Run shell commands",
process: "Manage background bash sessions",
whatsapp_login: "Generate and wait for WhatsApp QR login",
// Provider docking: add provider login tools here when a provider needs interactive linking.
browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas",
nodes: "List/describe/notify/camera/screen on paired nodes",
@@ -78,7 +82,6 @@ export function buildAgentSystemPrompt(params: {
"ls",
"bash",
"process",
"whatsapp_login",
"browser",
"canvas",
"nodes",
@@ -107,17 +110,25 @@ export function buildAgentSystemPrompt(params: {
const normalizedTools = canonicalToolNames.map((tool) => tool.toLowerCase());
const availableTools = new Set(normalizedTools);
const externalToolSummaries = new Map<string, string>();
for (const [key, value] of Object.entries(params.toolSummaries ?? {})) {
const normalized = key.trim().toLowerCase();
if (!normalized || !value?.trim()) continue;
externalToolSummaries.set(normalized, value.trim());
}
const extraTools = Array.from(
new Set(normalizedTools.filter((tool) => !toolOrder.includes(tool))),
);
const enabledTools = toolOrder.filter((tool) => availableTools.has(tool));
const toolLines = enabledTools.map((tool) => {
const summary = toolSummaries[tool];
const summary = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
const name = resolveToolName(tool);
return summary ? `- ${name}: ${summary}` : `- ${name}`;
});
for (const tool of extraTools.sort()) {
toolLines.push(`- ${resolveToolName(tool)}`);
const summary = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
const name = resolveToolName(tool);
toolLines.push(summary ? `- ${name}: ${summary}` : `- ${name}`);
}
const hasGateway = availableTools.has("gateway");
@@ -160,9 +171,7 @@ export function buildAgentSystemPrompt(params: {
const runtimeCapabilitiesLower = new Set(
runtimeCapabilities.map((cap) => cap.toLowerCase()),
);
const telegramInlineButtonsEnabled =
runtimeProvider === "telegram" &&
runtimeCapabilitiesLower.has("inlinebuttons");
const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons");
const skillsLines = skillsPrompt ? [skillsPrompt, ""] : [];
const skillsSection = skillsPrompt
? [
@@ -188,7 +197,6 @@ export function buildAgentSystemPrompt(params: {
"- ls: list directory contents",
`- ${bashToolName}: run shell commands (supports background via yieldMs/background)`,
`- ${processToolName}: manage background bash sessions`,
"- whatsapp_login: generate a WhatsApp QR code and wait for linking",
"- browser: control clawd's dedicated browser",
"- canvas: present/eval/snapshot the Canvas",
"- nodes: list/describe/notify/camera/screen on paired nodes",
@@ -314,11 +322,11 @@ export function buildAgentSystemPrompt(params: {
"### message tool",
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.).",
"- For `action=send`, include `to` and `message`.",
"- If multiple providers are configured, pass `provider` (whatsapp|telegram|discord|slack|signal|imessage|msteams).",
telegramInlineButtonsEnabled
? "- Telegram: inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
: runtimeProvider === "telegram"
? '- Telegram: inline buttons NOT enabled. If you need them, ask to add "inlineButtons" to telegram.capabilities or telegram.accounts.<id>.capabilities.'
`- If multiple providers are configured, pass \`provider\` (${MESSAGE_PROVIDER_OPTIONS}).`,
inlineButtonsEnabled
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
: runtimeProvider
? `- Inline buttons not enabled for ${runtimeProvider}. If you need them, ask to add "inlineButtons" to ${runtimeProvider}.capabilities or ${runtimeProvider}.accounts.<id>.capabilities.`
: "",
]
.filter(Boolean)

View File

@@ -0,0 +1,13 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
export function buildToolSummaryMap(
tools: AgentTool[],
): Record<string, string> {
const summaries: Record<string, string> = {};
for (const tool of tools) {
const summary = tool.description?.trim() || tool.label?.trim();
if (!summary) continue;
summaries[tool.name.toLowerCase()] = summary;
}
return summaries;
}

View File

@@ -1,4 +1,8 @@
import { callGateway } from "../../gateway/call.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js";
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
@@ -39,7 +43,8 @@ export async function callGatewayTool<T = unknown>(
params,
timeoutMs: gateway.timeoutMs,
expectFinal: extra?.expectFinal,
clientName: "agent",
mode: "agent",
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
clientDisplayName: "agent",
mode: GATEWAY_CLIENT_MODES.BACKEND,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,8 @@
import { callGateway } from "../../gateway/call.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
import type { AnnounceTarget } from "./sessions-send-helpers.js";
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
@@ -10,9 +14,13 @@ export async function resolveAnnounceTarget(params: {
const parsedDisplay = resolveAnnounceTargetFromKey(params.displayKey);
const fallback = parsed ?? parsedDisplay ?? null;
// Most providers can derive (provider,to) from the session key directly.
// WhatsApp is special: we may need lastAccountId from the session store.
if (fallback && fallback.provider !== "whatsapp") return fallback;
if (fallback) {
const normalized = normalizeProviderId(fallback.provider);
const plugin = normalized ? getProviderPlugin(normalized) : null;
if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) {
return fallback;
}
}
try {
const list = (await callGateway({

View File

@@ -1,4 +1,8 @@
import type { ClawdbotConfig } from "../../config/config.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
@@ -25,14 +29,19 @@ export function resolveAnnounceTargetFromKey(
const id = rest.join(":").trim();
if (!id) return null;
if (!providerRaw) return null;
const provider = providerRaw.toLowerCase();
if (provider === "discord") {
return { provider, to: `channel:${id}` };
}
if (provider === "signal") {
return { provider, to: `group:${id}` };
}
return { provider, to: id };
const normalizedProvider = normalizeProviderId(providerRaw);
const provider = normalizedProvider ?? providerRaw.toLowerCase();
const kindTarget = normalizedProvider
? kind === "channel"
? `channel:${id}`
: `group:${id}`
: id;
const normalized = normalizedProvider
? getProviderPlugin(normalizedProvider)?.messaging?.normalizeTarget?.(
kindTarget,
)
: undefined;
return { provider, to: normalized ?? kindTarget };
}
export function buildAgentToAgentMessageContext(params: {