diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb8d3da544..44f3691a3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Tests/Docker: add Codex on-demand install and live plugin-tool dependency E2E lanes for packaged onboarding and npm-pack plugin proof. - Plugins/ACPX: accept an optional `args` array in `agents.` config so paths and flag values containing spaces stay intact when spawning ACP agent processes. Thanks @TheArchitectit and @BunsDev. - Agents: inject the current provider/model identity into system prompts, including configured prompt overrides and CLI hook prompt replacements, so agents can answer model-identity questions from the actual runtime selection. +- Agents/subagents: add prompt-only `agents.defaults.subagents.delegationMode` and per-agent overrides with `suggest`/`prefer` modes, and centralize config-backed system prompt resolution across embedded, CLI, compaction, and command-export prompt surfaces. - Plugins/CLI: add the optional bundled `oc-path` plugin, providing `openclaw path` for surgical `oc://` access to markdown, JSONC, and JSONL workspace files. - Plugins/SDK: add unified model catalog registration for text, image, video, and music providers, including `providerCatalogEntry` manifests, shared media list help, live catalog caching, and per-model video capability overlays. - Plugin SDK: add presentation helpers for controls-only interactive rendering and opt-in empty fallback text so rich channel renderers can share `MessagePresentation` semantics without duplicating native cards or components. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 9c01ead4114..973370aa321 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -7d7ecfff72edaa6125c2b3e858c3f6dfbcc8942bb19abd8fee22797f618199f5 config-baseline.json -eec702624d26e2c5d6fda7cbcb573beaad223dd549c7f2927d4220f893fbe7a0 config-baseline.core.json +bb53a92a54a804d217baf466a4731924653d769db37122c38400cc3b97720c23 config-baseline.json +3b632b0f038846722e2a5012a5eeec2a29048b6e385b591d7bd9122aa0981a20 config-baseline.core.json 9edc62ae7dfedabc645470dd03102b813fc780b9108caf675fd661104714206f config-baseline.channel.json 1da42cb10427fb08510f29732493d24851ab915a424f91556569febdd450d9c3 config-baseline.plugin.json diff --git a/docs/concepts/parallel-specialist-lanes.md b/docs/concepts/parallel-specialist-lanes.md index 461d27a50a3..f1760b269a5 100644 --- a/docs/concepts/parallel-specialist-lanes.md +++ b/docs/concepts/parallel-specialist-lanes.md @@ -57,7 +57,7 @@ Tune queue and model capacity around the business value of each lane: agents: { defaults: { maxConcurrent: 4, - subagents: { maxConcurrent: 8 }, + subagents: { maxConcurrent: 8, delegationMode: "prefer" }, }, }, messages: { diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 921103289dd..41655311818 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -10,6 +10,20 @@ OpenClaw builds a custom system prompt for every agent run. The prompt is **Open The prompt is assembled by OpenClaw and injected into each agent run. +Prompt assembly has three layers: + +- `buildAgentSystemPrompt` renders the prompt from explicit inputs. It should + stay a pure renderer and should not read global config directly. +- `resolveAgentSystemPromptConfig` resolves config-backed prompt knobs such as + owner display, TTS hints, model aliases, memory citation mode, and sub-agent + delegation mode for a specific agent. +- Runtime adapters (embedded, CLI, command/export previews, compaction) gather + live facts such as tools, sandbox state, channel capabilities, context files, + and provider prompt contributions, then call the configured prompt facade. + +This keeps exported/debug prompt surfaces aligned with live runs without +turning every runtime-specific detail into one monolithic builder. + Provider plugins can contribute cache-aware prompt guidance without replacing the full OpenClaw-owned prompt. The provider runtime can: @@ -77,6 +91,13 @@ The Tooling section also includes runtime guidance for long-running work: - do not poll `subagents list` / `sessions_list` in a loop just to wait for completion +`agents.defaults.subagents.delegationMode` can strengthen this guidance. The +default `suggest` mode keeps the baseline nudge. `prefer` adds a dedicated +**Sub-Agent Delegation** section telling the main agent to act as a responsive +coordinator and push anything more involved than a direct reply through +`sessions_spawn`. This is prompt-only; tool policy still controls whether +`sessions_spawn` is available. + When the experimental `update_plan` tool is enabled, Tooling also tells the model to use it only for non-trivial multi-step work, keep exactly one `in_progress` step, and avoid repeating the whole plan after each update. diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index f7f484922ae..56a5ee5694e 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -143,6 +143,34 @@ session to confirm the effective tool list. - **Thinking:** inherits the caller unless you set `agents.defaults.subagents.thinking` (or per-agent `agents.list[].subagents.thinking`); an explicit `sessions_spawn.thinking` still wins. - **Run timeout:** if `sessions_spawn.runTimeoutSeconds` is omitted, OpenClaw uses `agents.defaults.subagents.runTimeoutSeconds` when set; otherwise it falls back to `0` (no timeout). +### Delegation prompt mode + +`agents.defaults.subagents.delegationMode` controls prompt guidance only; it does not change tool policy or enforce delegation. + +- `suggest` (default): keep the standard prompt nudge to use sub-agents for larger or slower work. +- `prefer`: tell the main agent to stay responsive and delegate anything more involved than a direct reply through `sessions_spawn`. + +Per-agent overrides use `agents.list[].subagents.delegationMode`. + +```json5 +{ + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + maxConcurrent: 4, + }, + }, + list: [ + { + id: "coordinator", + subagents: { delegationMode: "prefer" }, + }, + ], + }, +} +``` + ### Tool parameters diff --git a/src/agents/cli-runner/helpers.system-prompt.test.ts b/src/agents/cli-runner/helpers.system-prompt.test.ts new file mode 100644 index 00000000000..1fef66134b1 --- /dev/null +++ b/src/agents/cli-runner/helpers.system-prompt.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { buildCliAgentSystemPrompt } from "./helpers.js"; + +describe("buildCliAgentSystemPrompt", () => { + it("uses config-backed sub-agent delegation mode", () => { + const prompt = buildCliAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + config: { + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + }, + }, + }, + }, + agentId: "main", + tools: [{ name: "sessions_spawn" } as never], + modelDisplay: "test/model", + }); + + expect(prompt).toContain("## Sub-Agent Delegation"); + expect(prompt).toContain("Mode: prefer"); + }); +}); diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index 1293e9613de..45348096d30 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -19,17 +19,14 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "../../shared/string-coerce.js"; -import { buildTtsSystemPromptHint } from "../../tts/tts.js"; -import { buildModelAliasLines } from "../model-alias-lines.js"; import { resolveDefaultModelForAgent } from "../model-selection.js"; -import { resolveOwnerDisplaySetting } from "../owner-display.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import { detectImageReferences, loadImageFromRef } from "../pi-embedded-runner/run/images.js"; import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; import { detectRuntimeShell } from "../shell-utils.js"; import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js"; +import { buildConfiguredAgentSystemPrompt } from "../system-prompt-config.js"; import { buildSystemPromptParams } from "../system-prompt-params.js"; -import { buildAgentSystemPrompt } from "../system-prompt.js"; import type { SilentReplyPromptMode } from "../system-prompt.types.js"; import { sanitizeImageBlocks } from "../tool-images.js"; import { formatTomlConfigOverride } from "./toml-inline.js"; @@ -68,7 +65,7 @@ export function resolveCliRunQueueKey(params: { return params.backendId; } -export function buildSystemPrompt(params: { +export function buildCliAgentSystemPrompt(params: { workspaceDir: string; config?: OpenClawConfig; defaultThinkLevel?: ThinkLevel; @@ -105,19 +102,15 @@ export function buildSystemPrompt(params: { shell: detectRuntimeShell(), }, }); - const ttsHint = params.config - ? buildTtsSystemPromptHint(params.config, params.agentId) - : undefined; - const ownerDisplay = resolveOwnerDisplaySetting(params.config); - return buildAgentSystemPrompt({ + return buildConfiguredAgentSystemPrompt({ + config: params.config, + agentId: params.agentId, workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, extraSystemPrompt: params.extraSystemPrompt, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, silentReplyPromptMode: params.silentReplyPromptMode, ownerNumbers: params.ownerNumbers, - ownerDisplay: ownerDisplay.ownerDisplay, - ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint: false, heartbeatPrompt: params.heartbeatPrompt, docsPath: params.docsPath, @@ -125,17 +118,16 @@ export function buildSystemPrompt(params: { acpEnabled: isAcpRuntimeSpawnAvailable({ config: params.config }), runtimeInfo, toolNames: params.tools.map((tool) => tool.name), - modelAliasLines: buildModelAliasLines(params.config), skillsPrompt: params.skillsPrompt, userTimezone, userTime, userTimeFormat, contextFiles: params.contextFiles, - ttsHint, - memoryCitationsMode: params.config?.memory?.citations, }); } +export const buildSystemPrompt = buildCliAgentSystemPrompt; + export function normalizeCliModel(modelId: string, backend: CliBackendConfig): string { const trimmed = modelId.trim(); if (!trimmed) { diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 31680557c07..9a5671c13c3 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -46,7 +46,7 @@ import { buildSystemPromptReport } from "../system-prompt-report.js"; import { appendModelIdentitySystemPrompt } from "../system-prompt.js"; import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js"; import { prepareCliBundleMcpConfig } from "./bundle-mcp.js"; -import { buildSystemPrompt, normalizeCliModel } from "./helpers.js"; +import { buildCliAgentSystemPrompt, normalizeCliModel } from "./helpers.js"; import { cliBackendLog } from "./log.js"; import { buildCliSessionHistoryPrompt, @@ -334,7 +334,7 @@ export async function prepareCliRunContext( config: params.config, agentId: sessionAgentId, }) ?? - buildSystemPrompt({ + buildCliAgentSystemPrompt({ workspaceDir, config: params.config, defaultThinkLevel: params.thinkLevel, diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 174bc39071a..db91cf8d103 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -31,7 +31,6 @@ import { transformProviderSystemPrompt, } from "../../plugins/provider-runtime.js"; import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js"; -import { buildTtsSystemPromptHint } from "../../tts/tts.js"; import { resolveUserPath } from "../../utils.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; @@ -69,7 +68,6 @@ import { import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.js"; import { supportsModelTools } from "../model-tool-support.js"; import { ensureOpenClawModelsJson } from "../models-config.js"; -import { resolveOwnerDisplaySetting } from "../owner-display.js"; import { createBundleLspToolRuntime } from "../pi-bundle-lsp-runtime.js"; import { createBundleMcpToolRuntime } from "../pi-bundle-mcp-tools.js"; import { ensureSessionHeader } from "../pi-embedded-helpers.js"; @@ -140,7 +138,7 @@ import { log } from "./logger.js"; import { hardenManualCompactionBoundary } from "./manual-compaction-boundary.js"; import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js"; import { readPiModelContextTokens } from "./model-context-tokens.js"; -import { buildModelAliasLines, resolveModelAsync } from "./model.js"; +import { resolveModelAsync } from "./model.js"; import { sanitizeSessionHistory, validateReplayTurns } from "./replay-history.js"; import { buildEmbeddedSandboxInfo } from "./sandbox-info.js"; import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js"; @@ -859,10 +857,6 @@ async function compactEmbeddedPiSessionDirectOnce( cwd: effectiveWorkspace, moduleUrl: import.meta.url, }); - const ttsHint = params.config - ? buildTtsSystemPromptHint(params.config, sessionAgentId) - : undefined; - const ownerDisplay = resolveOwnerDisplaySetting(params.config); const promptContributionContext: Parameters< AgentRuntimePlan["prompt"]["resolveSystemPromptContribution"] >[0] = { @@ -885,13 +879,13 @@ async function compactEmbeddedPiSessionDirectOnce( agentId: sessionAgentId, }) ?? buildEmbeddedSystemPrompt({ + config: params.config, + agentId: sessionAgentId, workspaceDir: effectiveWorkspace, defaultThinkLevel, reasoningLevel: params.reasoningLevel ?? "off", extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, - ownerDisplay: ownerDisplay.ownerDisplay, - ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint, heartbeatPrompt: resolveHeartbeatPromptForSystemPrompt({ config: params.config, @@ -901,7 +895,6 @@ async function compactEmbeddedPiSessionDirectOnce( skillsPrompt, docsPath: openClawReferences.docsPath ?? undefined, sourcePath: openClawReferences.sourcePath ?? undefined, - ttsHint, promptMode, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, acpEnabled: isAcpRuntimeSpawnAvailable({ @@ -913,12 +906,10 @@ async function compactEmbeddedPiSessionDirectOnce( messageToolHints, sandboxInfo, tools: effectiveTools, - modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, userTimeFormat, contextFiles, - memoryCitationsMode: params.config?.memory?.citations, promptContribution, }); return createSystemPromptOverride( diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 55318b7f985..283a02851d2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -56,7 +56,6 @@ import { createTrajectoryRuntimeRecorder, toTrajectoryToolDefinitions, } from "../../../trajectory/runtime.js"; -import { buildTtsSystemPromptHint } from "../../../tts/tts.js"; import { resolveUserPath } from "../../../utils.js"; import { normalizeMessageChannel } from "../../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../../utils/provider-utils.js"; @@ -90,11 +89,9 @@ import { isTimeoutError } from "../../failover-error.js"; import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js"; import { resolveImageSanitizationLimits } from "../../image-sanitization.js"; import { stripHistoricalRuntimeContextCustomMessages } from "../../internal-runtime-context.js"; -import { buildModelAliasLines } from "../../model-alias-lines.js"; import { resolveModelAuthMode } from "../../model-auth.js"; import { resolveDefaultModelForAgent } from "../../model-selection.js"; import { supportsModelTools } from "../../model-tool-support.js"; -import { resolveOwnerDisplaySetting } from "../../owner-display.js"; import { createBundleLspToolRuntime } from "../../pi-bundle-lsp-runtime.js"; import { getOrCreateSessionMcpRuntime, @@ -1253,10 +1250,6 @@ export async function runEmbeddedAttempt( cwd: effectiveWorkspace, moduleUrl: import.meta.url, }); - const ttsHint = params.config - ? buildTtsSystemPromptHint(params.config, sessionAgentId) - : undefined; - const ownerDisplay = resolveOwnerDisplaySetting(params.config); const heartbeatPrompt = shouldInjectHeartbeatPrompt({ config: params.config, agentId: sessionAgentId, @@ -1303,19 +1296,18 @@ export async function runEmbeddedAttempt( systemPromptOverrideText, transformProviderSystemPrompt, embeddedSystemPrompt: { + config: params.config, + agentId: sessionAgentId, workspaceDir: effectiveWorkspace, defaultThinkLevel: params.thinkLevel, reasoningLevel: params.reasoningLevel ?? "off", extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, - ownerDisplay: ownerDisplay.ownerDisplay, - ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint, heartbeatPrompt, skillsPrompt: effectiveSkillsPrompt, docsPath: openClawReferences.docsPath ?? undefined, sourcePath: openClawReferences.sourcePath ?? undefined, - ttsHint, workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined, reactionGuidance, promptMode: effectivePromptMode, @@ -1330,7 +1322,6 @@ export async function runEmbeddedAttempt( messageToolHints, sandboxInfo, tools: effectiveTools, - modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, userTimeFormat, @@ -1338,7 +1329,6 @@ export async function runEmbeddedAttempt( bootstrapMode, bootstrapTruncationNotice, includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy", - memoryCitationsMode: params.config?.memory?.citations, promptContribution, }, providerTransform: { diff --git a/src/agents/pi-embedded-runner/system-prompt.test.ts b/src/agents/pi-embedded-runner/system-prompt.test.ts index be73a98306a..3a11ffbab4d 100644 --- a/src/agents/pi-embedded-runner/system-prompt.test.ts +++ b/src/agents/pi-embedded-runner/system-prompt.test.ts @@ -95,6 +95,37 @@ describe("buildEmbeddedSystemPrompt", () => { expect(prompt).toContain("## Embedded Stable\n\nStable provider guidance."); }); + it("uses config-backed sub-agent delegation mode", () => { + const prompt = buildEmbeddedSystemPrompt({ + config: { + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + }, + }, + }, + }, + agentId: "main", + workspaceDir: "/tmp/openclaw", + reasoningTagHint: false, + runtimeInfo: { + agentId: "main", + host: "local", + os: "darwin", + arch: "arm64", + node: process.version, + model: "gpt-5.4", + provider: "openai", + }, + tools: [{ name: "sessions_spawn" } as never], + userTimezone: "UTC", + }); + + expect(prompt).toContain("## Sub-Agent Delegation"); + expect(prompt).toContain("Mode: prefer"); + }); + it("can omit base memory guidance for non-legacy context engines", () => { registerMemoryPromptSection(() => ["## Memory Recall", "Use memory carefully.", ""]); diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 1b3b5982ca8..7b5166ed389 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -1,17 +1,21 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentSession } from "@mariozechner/pi-coding-agent"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; +import type { SubagentDelegationMode } from "../../config/types.agent-defaults.js"; import type { MemoryCitationsMode } from "../../config/types.memory.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { BootstrapMode } from "../bootstrap-mode.js"; import type { ResolvedTimeFormat } from "../date-time.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; +import { buildConfiguredAgentSystemPrompt } from "../system-prompt-config.js"; import type { ProviderSystemPromptContribution } from "../system-prompt-contribution.js"; -import { buildAgentSystemPrompt } from "../system-prompt.js"; import type { PromptMode, SilentReplyPromptMode } from "../system-prompt.types.js"; import type { EmbeddedSandboxInfo } from "./types.js"; import type { ReasoningLevel, ThinkLevel } from "./utils.js"; export function buildEmbeddedSystemPrompt(params: { + config?: OpenClawConfig; + agentId?: string; workspaceDir: string; defaultThinkLevel?: ThinkLevel; reasoningLevel?: ReasoningLevel; @@ -35,6 +39,8 @@ export function buildEmbeddedSystemPrompt(params: { /** Controls the generic silent-reply section. Channel-aware prompts can set "none". */ silentReplyPromptMode?: SilentReplyPromptMode; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; + /** Prompt-only strength for delegating non-trivial work through sub-agents. */ + subagentDelegationMode?: SubagentDelegationMode; /** Whether ACP-specific routing guidance should be included. Defaults to true. */ acpEnabled?: boolean; /** Registered runtime slash/native command names such as `codex`. */ @@ -57,7 +63,7 @@ export function buildEmbeddedSystemPrompt(params: { messageToolHints?: string[]; sandboxInfo?: EmbeddedSandboxInfo; tools: AgentTool[]; - modelAliasLines: string[]; + modelAliasLines?: string[]; userTimezone: string; userTime?: string; userTimeFormat?: ResolvedTimeFormat; @@ -68,7 +74,9 @@ export function buildEmbeddedSystemPrompt(params: { memoryCitationsMode?: MemoryCitationsMode; promptContribution?: ProviderSystemPromptContribution; }): string { - return buildAgentSystemPrompt({ + return buildConfiguredAgentSystemPrompt({ + config: params.config, + agentId: params.agentId ?? params.runtimeInfo.agentId, workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, reasoningLevel: params.reasoningLevel, @@ -87,6 +95,7 @@ export function buildEmbeddedSystemPrompt(params: { promptMode: params.promptMode, silentReplyPromptMode: params.silentReplyPromptMode, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, + subagentDelegationMode: params.subagentDelegationMode, acpEnabled: params.acpEnabled, nativeCommandNames: params.nativeCommandNames, nativeCommandGuidanceLines: params.nativeCommandGuidanceLines, diff --git a/src/agents/system-prompt-config.test.ts b/src/agents/system-prompt-config.test.ts new file mode 100644 index 00000000000..45538d27187 --- /dev/null +++ b/src/agents/system-prompt-config.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + buildConfiguredAgentSystemPrompt, + resolveAgentSystemPromptConfig, +} from "./system-prompt-config.js"; + +describe("resolveAgentSystemPromptConfig", () => { + it("defaults sub-agent delegation mode to suggest", () => { + expect(resolveAgentSystemPromptConfig({ config: {} }).subagentDelegationMode).toBe("suggest"); + }); + + it("inherits default sub-agent delegation mode", () => { + const config = { + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + }, + }, + }, + } satisfies OpenClawConfig; + + expect(resolveAgentSystemPromptConfig({ config, agentId: "main" })).toMatchObject({ + subagentDelegationMode: "prefer", + }); + }); + + it("lets per-agent sub-agent delegation mode override defaults", () => { + const config = { + agents: { + defaults: { + subagents: { + delegationMode: "suggest", + }, + }, + list: [ + { + id: "coordinator", + subagents: { + delegationMode: "prefer", + }, + }, + ], + }, + } satisfies OpenClawConfig; + + expect(resolveAgentSystemPromptConfig({ config, agentId: "coordinator" })).toMatchObject({ + subagentDelegationMode: "prefer", + }); + }); +}); + +describe("buildConfiguredAgentSystemPrompt", () => { + it("applies config-backed prompt parameters through the canonical facade", () => { + const prompt = buildConfiguredAgentSystemPrompt({ + config: { + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + }, + }, + }, + }, + agentId: "main", + workspaceDir: "/tmp/openclaw", + toolNames: ["sessions_spawn", "subagents"], + }); + + expect(prompt).toContain("## Sub-Agent Delegation"); + expect(prompt).toContain("Mode: prefer"); + }); +}); diff --git a/src/agents/system-prompt-config.ts b/src/agents/system-prompt-config.ts new file mode 100644 index 00000000000..633ee2cc91a --- /dev/null +++ b/src/agents/system-prompt-config.ts @@ -0,0 +1,53 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { buildTtsSystemPromptHint } from "../tts/tts.js"; +import { resolveAgentConfig } from "./agent-scope.js"; +import { buildModelAliasLines } from "./model-alias-lines.js"; +import { resolveOwnerDisplaySetting } from "./owner-display.js"; +import { buildAgentSystemPrompt } from "./system-prompt.js"; + +type AgentSystemPromptRenderParams = Parameters[0]; + +export type ResolvedAgentSystemPromptConfig = Pick< + AgentSystemPromptRenderParams, + | "ownerDisplay" + | "ownerDisplaySecret" + | "subagentDelegationMode" + | "ttsHint" + | "modelAliasLines" + | "memoryCitationsMode" +>; + +export type ConfiguredAgentSystemPromptParams = AgentSystemPromptRenderParams & { + config?: OpenClawConfig; + agentId?: string; +}; + +export function resolveAgentSystemPromptConfig(params: { + config?: OpenClawConfig; + agentId?: string; +}): ResolvedAgentSystemPromptConfig { + const { config, agentId } = params; + const ownerDisplay = resolveOwnerDisplaySetting(config); + const agentSubagents = + config && agentId ? resolveAgentConfig(config, agentId)?.subagents : undefined; + return { + ownerDisplay: ownerDisplay.ownerDisplay, + ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, + subagentDelegationMode: + agentSubagents?.delegationMode ?? + config?.agents?.defaults?.subagents?.delegationMode ?? + "suggest", + ttsHint: config ? buildTtsSystemPromptHint(config, agentId) : undefined, + modelAliasLines: buildModelAliasLines(config), + memoryCitationsMode: config?.memory?.citations, + }; +} + +export function buildConfiguredAgentSystemPrompt(params: ConfiguredAgentSystemPromptParams) { + const { config, agentId, ...renderParams } = params; + const configParams = config ? resolveAgentSystemPromptConfig({ config, agentId }) : {}; + return buildAgentSystemPrompt({ + ...renderParams, + ...configParams, + }); +} diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index dee34af702d..0637e525e8e 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -784,6 +784,40 @@ describe("buildAgentSystemPrompt", () => { ); }); + it("adds stronger sub-agent delegation guidance in prefer mode", () => { + const defaultPrompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + toolNames: ["sessions_spawn", "subagents"], + }); + const preferPrompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + toolNames: ["sessions_spawn", "subagents"], + subagentDelegationMode: "prefer", + }); + + expect(defaultPrompt).not.toContain("## Sub-Agent Delegation"); + expect(preferPrompt).toContain("## Sub-Agent Delegation"); + expect(preferPrompt).toContain("Mode: prefer"); + expect(preferPrompt).toContain("responsive coordinator"); + expect(preferPrompt).toContain( + "Anything requiring more work than a direct reply should go through `sessions_spawn`", + ); + expect(preferPrompt).toContain( + "Use `subagents(action=list|steer|kill)` only when explicitly asked for status", + ); + }); + + it("omits prefer delegation guidance when sessions_spawn is unavailable", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + toolNames: ["subagents"], + subagentDelegationMode: "prefer", + }); + + expect(prompt).not.toContain("## Sub-Agent Delegation"); + expect(prompt).toContain("Sub-agent orchestration"); + }); + it("reapplies provider prompt contributions", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 0ddbeb66ac6..741b0339b6c 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -6,6 +6,7 @@ import { hasNativeApprovalPromptRuntimeCapability, isKnownNativeApprovalPromptChannel, } from "../channels/plugins/native-approval-prompt.js"; +import type { SubagentDelegationMode } from "../config/types.agent-defaults.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; import { buildMemoryPromptSection } from "../plugins/memory-state.js"; import { @@ -63,6 +64,34 @@ type StablePromptPrefixCacheEntry = { value: string; }; +function normalizeSubagentDelegationMode(mode?: SubagentDelegationMode): SubagentDelegationMode { + return mode === "prefer" ? "prefer" : "suggest"; +} + +function buildSubagentDelegationPreferenceSection(params: { + mode: SubagentDelegationMode; + isMinimal: boolean; + hasSessionsSpawn: boolean; + hasSubagents: boolean; +}): string[] { + if (params.isMinimal || params.mode !== "prefer" || !params.hasSessionsSpawn) { + return []; + } + return [ + "## Sub-Agent Delegation", + "Mode: prefer. You are the responsive coordinator for this conversation.", + "- Reply directly only for trivial chat, clarifying questions, or a short answer already known from current context.", + "- Anything requiring more work than a direct reply should go through `sessions_spawn`; avoid doing expensive tool calls yourself.", + "- Delegate file/code inspection, shell commands, web/browser use, long reads, debugging, coding, multi-step analysis, comparisons, non-trivial summarization, and background waiting.", + '- Give the child a clear task. Omit `context` for isolated children; set `context:"fork"` only when current transcript details matter.', + "- After spawning, do not poll for completion. Child completion is push-based and returns as a runtime event; synthesize that result for the user.", + params.hasSubagents + ? "- Use `subagents(action=list|steer|kill)` only when explicitly asked for status, or when debugging/intervening; never use it in a wait loop." + : "", + "", + ].filter(Boolean); +} + const stablePromptPrefixCache = new Map(); function cacheStablePromptPrefix(key: string, build: () => string): string { @@ -624,6 +653,8 @@ export function buildAgentSystemPrompt(params: { /** Controls the generic silent-reply section. Channel-aware prompts can set "none". */ silentReplyPromptMode?: SilentReplyPromptMode; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; + /** Prompt-only strength for delegating non-trivial work through sub-agents. Defaults to "suggest". */ + subagentDelegationMode?: SubagentDelegationMode; /** Whether ACP-specific routing guidance should be included. Defaults to true. */ acpEnabled?: boolean; /** Registered runtime slash/native command names such as `codex`. */ @@ -813,6 +844,7 @@ export function buildAgentSystemPrompt(params: { const messageChannelOptions = listDeliverableMessageChannels().join("|"); const promptMode = params.promptMode ?? "full"; const isMinimal = promptMode === "minimal" || promptMode === "none"; + const subagentDelegationMode = normalizeSubagentDelegationMode(params.subagentDelegationMode); const sourceMessageToolOnly = params.sourceReplyDeliveryMode === "message_tool_only"; const silentReplyPromptMode = sourceMessageToolOnly ? "none" @@ -901,6 +933,7 @@ export function buildAgentSystemPrompt(params: { threadBoundAcpSpawnEnabled, sourceMessageToolOnly, silentReplyPromptMode, + subagentDelegationMode, sandboxInfo: params.sandboxInfo, displayWorkspaceDir, workspaceGuidance, @@ -967,6 +1000,12 @@ export function buildAgentSystemPrompt(params: { : []), "Do not poll `subagents list` / `sessions_list` in a loop; only check status on-demand (for intervention, debugging, or when explicitly asked).", "", + ...buildSubagentDelegationPreferenceSection({ + mode: subagentDelegationMode, + isMinimal, + hasSessionsSpawn, + hasSubagents: availableTools.has("subagents"), + }), ...buildOverridablePromptSection({ override: providerSectionOverrides.interaction_style, fallback: [], diff --git a/src/auto-reply/reply/commands-system-prompt.test.ts b/src/auto-reply/reply/commands-system-prompt.test.ts index 8c9c1604d94..7bce84c0fbf 100644 --- a/src/auto-reply/reply/commands-system-prompt.test.ts +++ b/src/auto-reply/reply/commands-system-prompt.test.ts @@ -237,4 +237,31 @@ describe("resolveCommandsSystemPromptBundle", () => { }), ); }); + + it("uses config-backed prompt settings for the target agent", async () => { + vi.mocked(resolveSandboxRuntimeStatus).mockReturnValue({ + sandboxed: false, + mode: "off", + } as never); + createOpenClawCodingToolsMock.mockReturnValue([{ name: "sessions_spawn" }] as never); + const params = makeParams(); + params.cfg = { + agents: { + defaults: { + subagents: { + delegationMode: "prefer", + }, + }, + }, + }; + + await resolveCommandsSystemPromptBundle(params); + + expect(vi.mocked(buildAgentSystemPrompt)).toHaveBeenCalledWith( + expect.objectContaining({ + subagentDelegationMode: "prefer", + toolNames: ["sessions_spawn"], + }), + ); + }); }); diff --git a/src/auto-reply/reply/commands-system-prompt.ts b/src/auto-reply/reply/commands-system-prompt.ts index 426648c4263..01d0280e808 100644 --- a/src/auto-reply/reply/commands-system-prompt.ts +++ b/src/auto-reply/reply/commands-system-prompt.ts @@ -10,12 +10,11 @@ import { createOpenClawCodingTools } from "../../agents/pi-tools.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js"; import { getSkillsSnapshotVersion } from "../../agents/skills/refresh-state.js"; +import { buildConfiguredAgentSystemPrompt } from "../../agents/system-prompt-config.js"; 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 { 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"; @@ -146,9 +145,9 @@ export async function resolveCommandsSystemPromptBundle( }, } : { enabled: false }; - const ttsHint = params.cfg ? buildTtsSystemPromptHint(params.cfg, sessionAgentId) : undefined; - - const systemPrompt = buildAgentSystemPrompt({ + const systemPrompt = buildConfiguredAgentSystemPrompt({ + config: params.cfg, + agentId: sessionAgentId, workspaceDir, defaultThinkLevel: params.resolvedThinkLevel, reasoningLevel: params.resolvedReasoningLevel, @@ -156,14 +155,12 @@ export async function resolveCommandsSystemPromptBundle( ownerNumbers: undefined, reasoningTagHint: false, toolNames, - modelAliasLines: [], userTimezone, userTime, userTimeFormat, contextFiles: injectedFiles, skillsPrompt, heartbeatPrompt: undefined, - ttsHint, acpEnabled: isAcpRuntimeSpawnAvailable({ config: params.cfg, sandboxed: sandboxRuntime.sandboxed, @@ -171,7 +168,6 @@ export async function resolveCommandsSystemPromptBundle( nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), runtimeInfo, sandboxInfo, - memoryCitationsMode: params.cfg?.memory?.citations, }); return { systemPrompt, tools, skillsPrompt, bootstrapFiles, injectedFiles, sandboxRuntime }; diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 122e8b168f2..6373991d950 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -227,6 +227,10 @@ export const FIELD_HELP: Record = { "Shared default settings inherited by agents unless overridden per entry in agents.list. Use defaults to enforce consistent baseline behavior and reduce duplicated per-agent configuration.", "agents.defaults.skills": "Optional default skill allowlist inherited by agents that omit agents.list[].skills. Omit for unrestricted skills, set [] to give inheriting agents no skills, and remember explicit agents.list[].skills replaces this default instead of merging with it.", + "agents.defaults.subagents.delegationMode": + 'Prompt-only sub-agent delegation strength. "suggest" keeps the default guidance; "prefer" strongly instructs the main agent to delegate anything more involved than a direct reply via sessions_spawn.', + "agents.list[].subagents.delegationMode": + "Per-agent override for sub-agent delegation strength. Use this for coordinator agents that should stay responsive and push non-trivial work into spawned sub-agents.", "agents.defaults.contextLimits": "Focused per-agent-context budget defaults for selected high-volume excerpts and injected prompt blocks. Use this to tune bounded read/injection sizes without reopening any unbounded call paths.", "agents.defaults.contextLimits.memoryGetMaxChars": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 776451307d9..eab2e37a8dc 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -388,6 +388,8 @@ export const FIELD_LABELS: Record = { "skills.load.watch": "Watch Skills", "skills.load.watchDebounceMs": "Skills Watch Debounce (ms)", "agents.defaults.skills": "Skills", + "agents.defaults.subagents.delegationMode": "Sub-agent Delegation Mode", + "agents.list[].subagents.delegationMode": "Sub-agent Delegation Mode", "agents.defaults.workspace": "Workspace", "agents.defaults.repoRoot": "Repo Root", "agents.defaults.promptOverlays": "Prompt Overlays", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index f0658f86b0d..c45e49f9438 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -19,6 +19,7 @@ import type { MemorySearchConfig } from "./types.tools.js"; export type AgentContextInjection = "always" | "continuation-skip" | "never"; export type OptionalBootstrapFileName = "SOUL.md" | "USER.md" | "HEARTBEAT.md" | "IDENTITY.md"; export type EmbeddedPiExecutionContract = "default" | "strict-agentic"; +export type SubagentDelegationMode = "suggest" | "prefer"; export type Gpt5PromptOverlayConfig = { /** Friendly interaction-style layer for GPT-5-family models (default: friendly). */ @@ -420,6 +421,8 @@ export type AgentDefaultsConfig = { maxConcurrent?: number; /** Sub-agent defaults (spawned via sessions_spawn). */ subagents?: { + /** Prompt-only guidance for how strongly the main agent should delegate work. Default: "suggest". */ + delegationMode?: SubagentDelegationMode; /** Default allowlist of target agent ids for sessions_spawn. Use "*" to allow any. */ allowAgents?: string[]; /** Max concurrent sub-agent runs (global lane: "subagent"). Default: 1. */ diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index 0ad77be4293..bdaffebbce2 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -4,6 +4,7 @@ import type { AgentDefaultsConfig, AgentModelEntryConfig, EmbeddedPiExecutionContract, + SubagentDelegationMode, } from "./types.agent-defaults.js"; import type { AgentEmbeddedHarnessConfig, @@ -116,6 +117,8 @@ export type AgentConfig = { identity?: IdentityConfig; groupChat?: GroupChatConfig; subagents?: { + /** Prompt-only guidance for how strongly this agent should delegate work. */ + delegationMode?: SubagentDelegationMode; /** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */ allowAgents?: string[]; /** Per-agent default model for spawned sub-agents (string or {primary,fallbacks}). */ diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 01e776291c6..8a1c1bcd566 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -32,6 +32,32 @@ describe("agent defaults schema", () => { ).toMatchObject({ success: true }); }); + it("accepts subagent delegation mode on defaults and agent entries", () => { + expect( + AgentDefaultsSchema.safeParse({ + subagents: { + delegationMode: "prefer", + }, + }), + ).toMatchObject({ success: true }); + expect( + AgentEntrySchema.safeParse({ + id: "coordinator", + subagents: { + delegationMode: "suggest", + }, + }), + ).toMatchObject({ success: true }); + expectSchemaFailurePath( + AgentDefaultsSchema.safeParse({ + subagents: { + delegationMode: "required", + }, + }), + "subagents.delegationMode", + ); + }); + it("accepts videoGenerationModel", () => { expect( AgentDefaultsSchema.safeParse({ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 2f2a99655fd..9621509db44 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -259,6 +259,7 @@ export const AgentDefaultsSchema = z maxConcurrent: z.number().int().positive().optional(), subagents: z .object({ + delegationMode: z.enum(["suggest", "prefer"]).optional(), allowAgents: z.array(z.string()).optional(), maxConcurrent: z.number().int().positive().optional(), maxSpawnDepth: z diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 72d477a772b..af3a03341ca 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -879,6 +879,7 @@ export const AgentEntrySchema = z groupChat: GroupChatSchema, subagents: z .object({ + delegationMode: z.enum(["suggest", "prefer"]).optional(), allowAgents: z.array(z.string()).optional(), model: z .union([