feat: add subagent delegation preference mode

This commit is contained in:
Peter Steinberger
2026-05-09 14:58:56 +01:00
parent 42033929d4
commit 24e1bbc014
25 changed files with 406 additions and 55 deletions

View File

@@ -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.<name>` 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.

View File

@@ -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

View File

@@ -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: {

View File

@@ -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.

View File

@@ -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
<ParamField path="task" type="string" required>

View File

@@ -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");
});
});

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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(

View File

@@ -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: {

View File

@@ -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.", ""]);

View File

@@ -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,

View File

@@ -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");
});
});

View File

@@ -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<typeof buildAgentSystemPrompt>[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,
});
}

View File

@@ -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",

View File

@@ -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<string, StablePromptPrefixCacheEntry>();
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: [],

View File

@@ -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"],
}),
);
});
});

View File

@@ -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 };

View File

@@ -227,6 +227,10 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@@ -388,6 +388,8 @@ export const FIELD_LABELS: Record<string, string> = {
"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",

View File

@@ -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. */

View File

@@ -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}). */

View File

@@ -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({

View File

@@ -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

View File

@@ -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([