diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index e5b23fb4e6d..1b1467396fd 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -8371f19a19ceeae4eb20fbfe8e68e51f6f54f42c487d7d5c75f214ab1ba0922a plugin-sdk-api-baseline.json -a5f5e15e75f8cf27ebaa1302cfe0488974edd53121279c1e90705bc531a4761a plugin-sdk-api-baseline.jsonl +6820a26fa0570caae81669337ec98f4946c096036e5603248c71a7afa4cb12a1 plugin-sdk-api-baseline.json +a3959468d81184676d92af7f8958c680ce351ac81d4bafade4428b0cb9609e65 plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 81bb056de97..214561c27b1 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -463,7 +463,7 @@ Keep capability registration public. Trim non-contract helper exports: - vendor-specific convenience helpers - setup/onboarding helpers that are implementation details -Some bundled-plugin helper subpaths still remain in the generated SDK export map for compatibility and bundled-plugin maintenance. Current examples include `plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup`, and several `plugin-sdk/matrix*` seams. Treat those as reserved implementation-detail exports, not as the recommended SDK pattern for new third-party plugins. +Some bundled-plugin helper subpaths still remain in the generated SDK export map for compatibility and bundled-plugin maintenance. Current examples include `plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup`, `plugin-sdk/channel-config-schema-legacy`, and several `plugin-sdk/matrix*` seams. Treat those as deprecated reserved exports, not as the recommended SDK pattern for new third-party plugins. ## Internals and reference diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 2ee33fc069f..9e5f5d6001e 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -251,7 +251,8 @@ releases. | `plugin-sdk/channel-pairing` | DM pairing primitives | `createChannelPairingController` | | `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring | `createChannelReplyPipeline` | | `plugin-sdk/channel-config-helpers` | Config adapter factories | `createHybridChannelConfigAdapter` | - | `plugin-sdk/channel-config-schema` | Config schema builders | Shared channel config schema primitives; bundled-channel-named schema exports are legacy compatibility only | + | `plugin-sdk/channel-config-schema` | Config schema builders | Shared channel config schema primitives and the generic builder only | + | `plugin-sdk/channel-config-schema-legacy` | Deprecated bundled config schemas | Bundled compatibility only; new plugins must define plugin-local schemas | | `plugin-sdk/telegram-command-config` | Telegram command config helpers | Command-name normalization, description trimming, duplicate/conflict validation | | `plugin-sdk/channel-policy` | Group/DM policy resolution | `resolveChannelGroupRequireMention` | | `plugin-sdk/channel-lifecycle` | Account status and draft stream lifecycle helpers | `createAccountStatusSink`, draft preview finalization helpers | diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index b96e58c2ca2..619adf93f8b 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -37,9 +37,9 @@ the broader umbrella surface and shared helpers such as For channel config, publish the channel-owned JSON Schema through `openclaw.plugin.json#channelConfigs`. The `plugin-sdk/channel-config-schema` -subpath is for shared schema primitives and the generic builder. Any -bundled-channel-named schema exports on that subpath are legacy compatibility -exports, not a pattern for new plugins. +subpath is for shared schema primitives and the generic builder. Deprecated +bundled-channel schema exports live on `plugin-sdk/channel-config-schema-legacy` +for bundled compatibility only; they are not a pattern for new plugins. Do not import provider- or channel-branded convenience seams (for example @@ -94,6 +94,10 @@ methods: | `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) | | `api.registerCommand(def)` | Custom command (bypasses the LLM) | +Plugin commands can set `agentPromptGuidance` when the agent needs a short, +command-owned routing hint. Keep that text about the command itself; do not add +provider- or plugin-specific policy to core prompt builders. + ### Infrastructure | Method | What it registers | diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 38c1b725cbf..239f92dd0c3 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -43,7 +43,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | `plugin-sdk/channel-pairing` | `createChannelPairingController` | | `plugin-sdk/channel-reply-pipeline` | `createChannelReplyPipeline` | | `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter` | - | `plugin-sdk/channel-config-schema` | Channel config schema types | + | `plugin-sdk/channel-config-schema` | Shared channel config schema primitives and generic builder | + | `plugin-sdk/channel-config-schema-legacy` | Deprecated bundled-channel config schemas for bundled compatibility only | | `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback | | `plugin-sdk/command-gating` | Narrow command authorization gate helpers | | `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` | @@ -272,7 +273,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | Matrix | `plugin-sdk/matrix`, `plugin-sdk/matrix-helper`, `plugin-sdk/matrix-runtime-heavy`, `plugin-sdk/matrix-runtime-shared`, `plugin-sdk/matrix-runtime-surface`, `plugin-sdk/matrix-surface`, `plugin-sdk/matrix-thread-bindings` | Bundled Matrix helper/runtime surface | | Line | `plugin-sdk/line`, `plugin-sdk/line-core`, `plugin-sdk/line-runtime`, `plugin-sdk/line-surface` | Bundled LINE helper/runtime surface | | IRC | `plugin-sdk/irc`, `plugin-sdk/irc-surface` | Bundled IRC helper surface | - | Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Bundled channel compatibility/helper seams | + | Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Deprecated bundled channel compatibility/helper seams. New plugins should import generic SDK subpaths or plugin-local barrels. | | Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` | diff --git a/extensions/codex/src/commands.ts b/extensions/codex/src/commands.ts index 7a1e147a90f..f45c5dbbea0 100644 --- a/extensions/codex/src/commands.ts +++ b/extensions/codex/src/commands.ts @@ -12,6 +12,10 @@ export function createCodexCommand(options: { return { name: "codex", description: "Inspect and control the Codex app-server harness", + agentPromptGuidance: [ + "Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.", + "Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.", + ], acceptsArgs: true, requireAuth: true, handler: (ctx) => handleCodexCommand(ctx, options), diff --git a/extensions/discord/config-api.ts b/extensions/discord/config-api.ts index dc40b4d0c5e..67bd2a9e7c2 100644 --- a/extensions/discord/config-api.ts +++ b/extensions/discord/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, DiscordConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/extensions/googlechat/src/config-schema.test.ts b/extensions/googlechat/src/config-schema.test.ts index 1e7ebbd2cb3..93ab1d15d2a 100644 --- a/extensions/googlechat/src/config-schema.test.ts +++ b/extensions/googlechat/src/config-schema.test.ts @@ -1,5 +1,5 @@ -import { GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat"; import { describe, expect, it } from "vitest"; +import { GoogleChatConfigSchema } from "../runtime-api.js"; describe("googlechat config schema", () => { it("accepts serviceAccount refs", () => { diff --git a/extensions/googlechat/src/config-schema.ts b/extensions/googlechat/src/config-schema.ts index 686d4a0b99c..93c43b2e25c 100644 --- a/extensions/googlechat/src/config-schema.ts +++ b/extensions/googlechat/src/config-schema.ts @@ -1,3 +1,3 @@ -import { buildChannelConfigSchema, GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat"; +import { buildChannelConfigSchema, GoogleChatConfigSchema } from "../runtime-api.js"; export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema); diff --git a/extensions/imessage/config-api.ts b/extensions/imessage/config-api.ts index f9eff71f741..0fae904952c 100644 --- a/extensions/imessage/config-api.ts +++ b/extensions/imessage/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, IMessageConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/extensions/msteams/config-api.ts b/extensions/msteams/config-api.ts index 46d9afd4db2..618418db768 100644 --- a/extensions/msteams/config-api.ts +++ b/extensions/msteams/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, MSTeamsConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/extensions/signal/config-api.ts b/extensions/signal/config-api.ts index 1075a2e0a1c..cff779ae393 100644 --- a/extensions/signal/config-api.ts +++ b/extensions/signal/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, SignalConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/extensions/slack/config-api.ts b/extensions/slack/config-api.ts index ac8bc459d7f..a7662885d05 100644 --- a/extensions/slack/config-api.ts +++ b/extensions/slack/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, SlackConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/extensions/telegram/config-api.ts b/extensions/telegram/config-api.ts index ce51184de13..bba1c3e8036 100644 --- a/extensions/telegram/config-api.ts +++ b/extensions/telegram/config-api.ts @@ -1,7 +1,7 @@ export { buildChannelConfigSchema, TelegramConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; export { normalizeTelegramCommandDescription, normalizeTelegramCommandName, diff --git a/extensions/whatsapp/config-api.ts b/extensions/whatsapp/config-api.ts index f3692fa590d..fa8a52e7fc6 100644 --- a/extensions/whatsapp/config-api.ts +++ b/extensions/whatsapp/config-api.ts @@ -1,4 +1,4 @@ export { buildChannelConfigSchema, WhatsAppConfigSchema, -} from "openclaw/plugin-sdk/channel-config-schema"; +} from "openclaw/plugin-sdk/channel-config-schema-legacy"; diff --git a/package.json b/package.json index 02857590542..133d08f10d1 100644 --- a/package.json +++ b/package.json @@ -642,6 +642,10 @@ "types": "./dist/plugin-sdk/channel-config-schema.d.ts", "default": "./dist/plugin-sdk/channel-config-schema.js" }, + "./plugin-sdk/channel-config-schema-legacy": { + "types": "./dist/plugin-sdk/channel-config-schema-legacy.d.ts", + "default": "./dist/plugin-sdk/channel-config-schema-legacy.js" + }, "./plugin-sdk/channel-actions": { "types": "./dist/plugin-sdk/channel-actions.d.ts", "default": "./dist/plugin-sdk/channel-actions.js" diff --git a/scripts/lib/plugin-sdk-doc-metadata.ts b/scripts/lib/plugin-sdk-doc-metadata.ts index 82f0170f710..7a9c167c838 100644 --- a/scripts/lib/plugin-sdk-doc-metadata.ts +++ b/scripts/lib/plugin-sdk-doc-metadata.ts @@ -47,6 +47,9 @@ export const pluginSdkDocMetadata = { "channel-config-schema": { category: "channel", }, + "channel-config-schema-legacy": { + category: "channel", + }, "channel-contract": { category: "channel", }, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 1522d709929..31b8e9eec99 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -144,6 +144,7 @@ "channel-config-writes", "channel-config-primitives", "channel-config-schema", + "channel-config-schema-legacy", "channel-actions", "channel-plugin-common", "channel-core", diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 5c4d60ff368..c612e432762 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -21,7 +21,7 @@ import { formatErrorMessage } from "../../../infra/errors.js"; import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js"; import { getMachineDisplayName } from "../../../infra/machine-name.js"; import { MAX_IMAGE_BYTES } from "../../../media/constants.js"; -import { listRegisteredPluginCommands } from "../../../plugins/command-registry-state.js"; +import { listRegisteredPluginAgentPromptGuidance } from "../../../plugins/command-registry-state.js"; import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; import { extractModelCompat, @@ -1132,7 +1132,7 @@ export async function runEmbeddedAttempt( config: params.config, sandboxed: sandboxInfo?.enabled === true, }), - nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name), + nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), runtimeInfo, messageToolHints, sandboxInfo, diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index cc0ec414779..674e93911fc 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -34,6 +34,8 @@ export function buildEmbeddedSystemPrompt(params: { acpEnabled?: boolean; /** Registered runtime slash/native command names such as `codex`. */ nativeCommandNames?: string[]; + /** Plugin-owned prompt guidance for registered native slash commands. */ + nativeCommandGuidanceLines?: string[]; runtimeInfo: { agentId?: string; host: string; @@ -79,6 +81,7 @@ export function buildEmbeddedSystemPrompt(params: { promptMode: params.promptMode, acpEnabled: params.acpEnabled, nativeCommandNames: params.nativeCommandNames, + nativeCommandGuidanceLines: params.nativeCommandGuidanceLines, runtimeInfo: params.runtimeInfo, messageToolHints: params.messageToolHints, sandboxInfo: params.sandboxInfo, diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 328f7779b26..6e3e69ce782 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -5,7 +5,7 @@ import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { SubagentSpawnPreparation } from "../context-engine/types.js"; -import { listRegisteredPluginCommands } from "../plugins/command-registry-state.js"; +import { listRegisteredPluginAgentPromptGuidance } from "../plugins/command-registry-state.js"; import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js"; import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; import { @@ -931,7 +931,7 @@ export async function spawnSubagentDirect( config: cfg, sandboxed: childRuntime.sandboxed, }), - nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name), + nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), childDepth, maxSpawnDepth, }); diff --git a/src/agents/subagent-system-prompt.ts b/src/agents/subagent-system-prompt.ts index 555230b17dd..4984b757339 100644 --- a/src/agents/subagent-system-prompt.ts +++ b/src/agents/subagent-system-prompt.ts @@ -11,6 +11,8 @@ export function buildSubagentSystemPrompt(params: { acpEnabled?: boolean; /** Registered runtime slash/native command names such as `codex`. */ nativeCommandNames?: string[]; + /** Plugin-owned prompt guidance for registered native slash commands. */ + nativeCommandGuidanceLines?: string[]; /** Depth of the child being spawned (1 = sub-agent, 2 = sub-sub-agent). */ childDepth?: number; /** Config value: max allowed spawn depth. */ @@ -25,8 +27,8 @@ export function buildSubagentSystemPrompt(params: { ? params.maxSpawnDepth : DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH; const acpEnabled = params.acpEnabled === true; - const nativeCodexCommandAvailable = (params.nativeCommandNames ?? []).some( - (name) => name.trim().replace(/^\/+/, "").toLowerCase() === "codex", + const nativeCommandGuidanceLines = Array.from( + new Set((params.nativeCommandGuidanceLines ?? []).map((line) => line.trim()).filter(Boolean)), ); const canSpawn = childDepth < maxSpawnDepth; const parentLabel = childDepth >= 2 ? "parent orchestrator" : "main agent"; @@ -95,11 +97,7 @@ export function buildSubagentSystemPrompt(params: { "Coordinate their work and synthesize results before reporting back.", ...(acpEnabled ? [ - ...(nativeCodexCommandAvailable - ? [ - "Native Codex app-server plugin is available (`/codex ...`). Prefer that path for Codex bind/control/thread/resume/steer/stop requests; use Codex ACP only when explicitly requested.", - ] - : []), + ...nativeCommandGuidanceLines, 'For ACP harness sessions (claudecode/gemini/opencode, or Codex only when explicit ACP/acpx), use `sessions_spawn` with `runtime: "acp"` (set `agentId` unless `acp.defaultAgent` is configured).', '`agents_list` and `subagents` apply to OpenClaw sub-agents (`runtime: "subagent"`); ACP harness ids are controlled by `acp.allowedAgents`.', "Do not ask users to run slash commands or CLI when `sessions_spawn` can do it directly.", diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 98777275937..d558aad99e8 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -335,7 +335,10 @@ describe("buildAgentSystemPrompt", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", toolNames: ["sessions_spawn", "subagents", "agents_list", "exec"], - nativeCommandNames: ["codex"], + nativeCommandGuidanceLines: [ + "Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.", + "Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.", + ], acpEnabled: true, }); @@ -1038,7 +1041,9 @@ describe("buildSubagentSystemPrompt", () => { task: "research task", childDepth: 1, maxSpawnDepth: 2, - nativeCommandNames: ["codex"], + nativeCommandGuidanceLines: [ + "Native Codex app-server plugin is available (`/codex ...`). Prefer that path for Codex bind/control/thread/resume/steer/stop requests; use Codex ACP only when explicitly requested.", + ], acpEnabled: true, }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 2f91cc155fd..059edb5dc41 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -381,14 +381,6 @@ function buildMessagingSection(params: { ]; } -function hasNativeCommand(params: { nativeCommandNames?: string[]; command: string }): boolean { - const target = normalizeLowercaseStringOrEmpty(params.command); - return (params.nativeCommandNames ?? []).some((name) => { - const normalized = normalizeLowercaseStringOrEmpty(name).replace(/^\/+/, ""); - return normalized === target; - }); -} - function buildVoiceSection(params: { isMinimal: boolean; ttsHint?: string }) { if (params.isMinimal) { return []; @@ -472,6 +464,8 @@ export function buildAgentSystemPrompt(params: { acpEnabled?: boolean; /** Registered runtime slash/native command names such as `codex`. */ nativeCommandNames?: string[]; + /** Plugin-owned prompt guidance for registered native slash commands. */ + nativeCommandGuidanceLines?: string[]; runtimeInfo?: { agentId?: string; host?: string; @@ -580,10 +574,9 @@ export function buildAgentSystemPrompt(params: { const availableTools = new Set(normalizedTools); const hasSessionsSpawn = availableTools.has("sessions_spawn"); const acpHarnessSpawnAllowed = hasSessionsSpawn && acpSpawnRuntimeEnabled; - const nativeCodexCommandAvailable = hasNativeCommand({ - nativeCommandNames: params.nativeCommandNames, - command: "codex", - }); + const nativeCommandGuidanceLines = Array.from( + new Set((params.nativeCommandGuidanceLines ?? []).map((line) => line.trim()).filter(Boolean)), + ); const externalToolSummaries = new Map(); for (const [key, value] of Object.entries(params.toolSummaries ?? {})) { const normalized = key.trim().toLowerCase(); @@ -733,12 +726,7 @@ export function buildAgentSystemPrompt(params: { `For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=).`, "If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.", 'Sub-agents start isolated by default. Use `sessions_spawn` with `context:"fork"` only when the child needs the current transcript context; otherwise omit `context` or use `context:"isolated"`.', - ...(nativeCodexCommandAvailable - ? [ - "Native Codex app-server plugin is available (`/codex ...`). For Codex bind/control/thread/resume/steer/stop requests, prefer `/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, and `/codex stop` over ACP.", - "Use ACP for Codex only when the user explicitly asks for ACP/acpx or wants to test the ACP path.", - ] - : []), + ...nativeCommandGuidanceLines, ...(acpHarnessSpawnAllowed ? [ 'For requests like "do this in claude code/cursor/gemini/opencode" or similar ACP harnesses, treat it as ACP harness intent and call `sessions_spawn` with `runtime: "acp"`.', diff --git a/src/auto-reply/reply/commands-system-prompt.ts b/src/auto-reply/reply/commands-system-prompt.ts index 04d11e12d63..426648c4263 100644 --- a/src/auto-reply/reply/commands-system-prompt.ts +++ b/src/auto-reply/reply/commands-system-prompt.ts @@ -14,7 +14,7 @@ import { buildSystemPromptParams } from "../../agents/system-prompt-params.js"; import { buildAgentSystemPrompt } from "../../agents/system-prompt.js"; import type { WorkspaceBootstrapFile } from "../../agents/workspace.js"; import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; -import { listRegisteredPluginCommands } from "../../plugins/command-registry-state.js"; +import { listRegisteredPluginAgentPromptGuidance } from "../../plugins/command-registry-state.js"; import { buildTtsSystemPromptHint } from "../../tts/tts.js"; import type { HandleCommandsParams } from "./commands-types.js"; import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js"; @@ -168,7 +168,7 @@ export async function resolveCommandsSystemPromptBundle( config: params.cfg, sandboxed: sandboxRuntime.sandboxed, }), - nativeCommandNames: listRegisteredPluginCommands().map((command) => command.name), + nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), runtimeInfo, sandboxInfo, memoryCitationsMode: params.cfg?.memory?.citations, diff --git a/src/plugin-sdk/channel-config-schema-legacy.ts b/src/plugin-sdk/channel-config-schema-legacy.ts new file mode 100644 index 00000000000..3fe6b5ff304 --- /dev/null +++ b/src/plugin-sdk/channel-config-schema-legacy.ts @@ -0,0 +1,34 @@ +/** + * Deprecated bundled-channel compatibility surface. + * + * New plugins should define plugin-local schemas and import primitives from + * openclaw/plugin-sdk/channel-config-schema instead of depending on these + * bundled channel schemas. + */ +export { + AllowFromListSchema, + buildChannelConfigSchema, + buildCatchallMultiAccountChannelSchema, + buildNestedDmConfigSchema, +} from "../channels/plugins/config-schema.js"; +export { + BlockStreamingCoalesceSchema, + ContextVisibilityModeSchema, + DmConfigSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + ReplyRuntimeConfigSchemaShape, + requireOpenAllowFrom, +} from "../config/zod-schema.core.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { + DiscordConfigSchema, + GoogleChatConfigSchema, + IMessageConfigSchema, + MSTeamsConfigSchema, + SignalConfigSchema, + SlackConfigSchema, + TelegramConfigSchema, +} from "../config/zod-schema.providers-core.js"; +export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js"; diff --git a/src/plugin-sdk/channel-config-schema.ts b/src/plugin-sdk/channel-config-schema.ts index 152db2f8ca0..363ff9c7685 100644 --- a/src/plugin-sdk/channel-config-schema.ts +++ b/src/plugin-sdk/channel-config-schema.ts @@ -16,16 +16,3 @@ export { requireOpenAllowFrom, } from "../config/zod-schema.core.js"; export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; -// Legacy bundled channel schema exports. New channel plugins should define -// plugin-local schemas and expose JSON Schema through openclaw.plugin.json -// channelConfigs or a lightweight plugin-owned config artifact. -export { - DiscordConfigSchema, - GoogleChatConfigSchema, - IMessageConfigSchema, - MSTeamsConfigSchema, - SignalConfigSchema, - SlackConfigSchema, - TelegramConfigSchema, -} from "../config/zod-schema.providers-core.js"; -export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js"; diff --git a/src/plugins/command-registration.ts b/src/plugins/command-registration.ts index 945a26f67a2..e8bc2876aaf 100644 --- a/src/plugins/command-registration.ts +++ b/src/plugins/command-registration.ts @@ -102,6 +102,17 @@ export function validatePluginCommandDefinition( if (!command.description.trim()) { return "Command description cannot be empty"; } + if (command.agentPromptGuidance !== undefined && !Array.isArray(command.agentPromptGuidance)) { + return "Agent prompt guidance must be an array of strings"; + } + for (const [index, guidance] of (command.agentPromptGuidance ?? []).entries()) { + if (typeof guidance !== "string") { + return `Agent prompt guidance ${index + 1} must be a string`; + } + if (!guidance.trim()) { + return `Agent prompt guidance ${index + 1} cannot be empty`; + } + } const nameError = validateCommandName(command.name.trim()); if (nameError) { return nameError; @@ -167,6 +178,9 @@ export function registerPluginCommand( ...command, name, description, + ...(command.agentPromptGuidance + ? { agentPromptGuidance: command.agentPromptGuidance.map((line) => line.trim()) } + : {}), }; const invocationKeys = listPluginInvocationKeys(normalizedCommand); const key = `/${normalizeLowercaseStringOrEmpty(name)}`; diff --git a/src/plugins/command-registry-state.ts b/src/plugins/command-registry-state.ts index 33fbdd06f69..0bdde72630b 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -57,6 +57,22 @@ export function listRegisteredPluginCommands(): RegisteredPluginCommand[] { return Array.from(pluginCommands.values()); } +export function listRegisteredPluginAgentPromptGuidance(): string[] { + const lines: string[] = []; + const seen = new Set(); + for (const command of pluginCommands.values()) { + for (const line of command.agentPromptGuidance ?? []) { + const trimmed = line.trim(); + if (!trimmed || seen.has(trimmed)) { + continue; + } + seen.add(trimmed); + lines.push(trimmed); + } + } + return lines; +} + export function restorePluginCommands(commands: readonly RegisteredPluginCommand[]): void { pluginCommands.clear(); for (const command of commands) { diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 0bda679304e..3e0c17d861b 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; +import { listRegisteredPluginAgentPromptGuidance } from "./command-registry-state.js"; import { __testing, clearPluginCommands, @@ -250,6 +251,19 @@ describe("registerPluginCommand", () => { error: "Command description must be a string", }, }, + { + name: "rejects invalid agent prompt guidance", + command: { + name: "demo", + description: "Demo", + agentPromptGuidance: "use /demo" as unknown as string[], + handler: async () => ({ text: "ok" }), + }, + expected: { + ok: false, + error: "Agent prompt guidance must be an array of strings", + }, + }, ] as const)("$name", ({ command, expected }) => { expect(registerPluginCommand("demo-plugin", command)).toEqual(expected); }); @@ -258,6 +272,7 @@ describe("registerPluginCommand", () => { const result = registerPluginCommand("demo-plugin", { name: " demo_cmd ", description: " Demo command ", + agentPromptGuidance: [" Use /demo_cmd for demo routing. "], handler: async () => ({ text: "ok" }), }); expect(result).toEqual({ ok: true }); @@ -276,6 +291,7 @@ describe("registerPluginCommand", () => { acceptsArgs: false, }, ]); + expect(listRegisteredPluginAgentPromptGuidance()).toEqual(["Use /demo_cmd for demo routing."]); }); it("matches underscore aliases for hyphenated command names", () => { diff --git a/src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts b/src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts index 985abfddbb5..fc56c5feaea 100644 --- a/src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts +++ b/src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts @@ -33,7 +33,7 @@ const BUNDLED_EXTENSION_CONFIG_IMPORT_GUARDS = [ }, { path: "extensions/googlechat/src/config-schema.ts", - allowedSpecifier: "openclaw/plugin-sdk/googlechat", + allowedSpecifier: "../runtime-api.js", }, // Teams keeps a package-local config barrel so production code does not // self-import via openclaw/plugin-sdk/msteams from inside the same extension. diff --git a/src/plugins/contracts/config-footprint-guardrails.test.ts b/src/plugins/contracts/config-footprint-guardrails.test.ts index 1fd5c1cdcd7..f70849e5b95 100644 --- a/src/plugins/contracts/config-footprint-guardrails.test.ts +++ b/src/plugins/contracts/config-footprint-guardrails.test.ts @@ -162,12 +162,15 @@ describe("config footprint guardrails", () => { ); }); - it("keeps bundled channel schemas as a fixed legacy SDK compatibility surface", () => { + it("keeps bundled channel schemas out of the generic channel config SDK surface", () => { const source = readSource("src/plugin-sdk/channel-config-schema.ts"); - const legacySection = source.slice(source.indexOf("Legacy bundled channel schema exports")); + const legacySource = readSource("src/plugin-sdk/channel-config-schema-legacy.ts"); + const legacySection = legacySource.slice( + legacySource.indexOf("Deprecated bundled-channel compatibility surface"), + ); const bundledSchemaExportBlocks = Array.from( legacySection.matchAll( - /export \{(?[\s\S]*?)\} from "\.\.\/config\/zod-schema\.providers-(?:core|whatsapp)\.js";/g, + /export \{(?[^}]*)\} from "\.\.\/config\/zod-schema\.providers-(?:core|whatsapp)\.js";/g, ), ) .map((match) => match.groups?.exports) @@ -190,6 +193,10 @@ describe("config footprint guardrails", () => { "TelegramConfigSchema", "WhatsAppConfigSchema", ]); - expect(source).toContain("Legacy bundled channel schema exports"); + for (const schemaName of exportedSchemaNames) { + expect(source).not.toContain(schemaName); + } + expect(legacySource).toContain("Deprecated bundled-channel compatibility surface"); + expect(legacySource).toContain("openclaw/plugin-sdk/channel-config-schema"); }); }); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 4bce34f90b7..0df545c102e 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1873,6 +1873,8 @@ export type OpenClawPluginCommandDefinition = { }; /** Description shown in /help and command menus */ description: string; + /** Optional system-prompt guidance for agents when this command is registered. */ + agentPromptGuidance?: readonly string[]; /** Whether this command accepts arguments */ acceptsArgs?: boolean; /** Whether only authorized senders can use this command (default: true) */