mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 06:41:44 +00:00
feat(agents): add provider-owned system prompt contributions
This commit is contained in:
@@ -18,7 +18,10 @@ import {
|
||||
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
||||
import { generateSecureToken } from "../../infra/secure-random.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.js";
|
||||
import {
|
||||
prepareProviderRuntimeAuth,
|
||||
resolveProviderSystemPromptContribution,
|
||||
} from "../../plugins/provider-runtime.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/types.js";
|
||||
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
|
||||
@@ -648,6 +651,22 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
});
|
||||
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
||||
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
||||
const promptContribution = resolveProviderSystemPromptContribution({
|
||||
provider,
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
context: {
|
||||
config: params.config,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
provider,
|
||||
modelId,
|
||||
promptMode,
|
||||
runtimeChannel,
|
||||
runtimeCapabilities,
|
||||
agentId: sessionAgentId,
|
||||
},
|
||||
});
|
||||
const buildSystemPromptOverride = (defaultThinkLevel: ThinkLevel) =>
|
||||
createSystemPromptOverride(
|
||||
buildEmbeddedSystemPrompt({
|
||||
@@ -678,6 +697,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
|
||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
||||
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
|
||||
import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js";
|
||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
@@ -656,6 +657,22 @@ export async function runEmbeddedAttempt(
|
||||
})
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined;
|
||||
const promptContribution = resolveProviderSystemPromptContribution({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
context: {
|
||||
config: params.config,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
promptMode: effectivePromptMode,
|
||||
runtimeChannel,
|
||||
runtimeCapabilities,
|
||||
agentId: sessionAgentId,
|
||||
},
|
||||
});
|
||||
|
||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
@@ -684,6 +701,7 @@ export async function runEmbeddedAttempt(
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
memoryCitationsMode: params.config?.memory?.citations,
|
||||
promptContribution,
|
||||
});
|
||||
const systemPromptReport = buildSystemPromptReport({
|
||||
source: "run",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applySystemPromptOverrideToSession, createSystemPromptOverride } from "./system-prompt.js";
|
||||
import {
|
||||
applySystemPromptOverrideToSession,
|
||||
buildEmbeddedSystemPrompt,
|
||||
createSystemPromptOverride,
|
||||
} from "./system-prompt.js";
|
||||
|
||||
type MutableSession = {
|
||||
_baseSystemPrompt?: string;
|
||||
@@ -61,3 +65,28 @@ describe("applySystemPromptOverrideToSession", () => {
|
||||
expect(mutable._rebuildSystemPrompt?.(["tool1"])).toBe("rebuild test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildEmbeddedSystemPrompt", () => {
|
||||
it("forwards provider prompt contributions into the embedded prompt", () => {
|
||||
const prompt = buildEmbeddedSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
reasoningTagHint: false,
|
||||
runtimeInfo: {
|
||||
host: "local",
|
||||
os: "darwin",
|
||||
arch: "arm64",
|
||||
node: process.version,
|
||||
model: "gpt-5.4",
|
||||
provider: "openai",
|
||||
},
|
||||
tools: [],
|
||||
modelAliasLines: [],
|
||||
userTimezone: "UTC",
|
||||
promptContribution: {
|
||||
stablePrefix: "## Embedded Stable\n\nStable provider guidance.",
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Embedded Stable\n\nStable provider guidance.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import type { MemoryCitationsMode } from "../../config/types.memory.js";
|
||||
import type { ResolvedTimeFormat } from "../date-time.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import type { ProviderSystemPromptContribution } from "../system-prompt-contribution.js";
|
||||
import { buildAgentSystemPrompt, type PromptMode } from "../system-prompt.js";
|
||||
import type { EmbeddedSandboxInfo } from "./types.js";
|
||||
import type { ReasoningLevel, ThinkLevel } from "./utils.js";
|
||||
@@ -51,6 +52,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
userTimeFormat?: ResolvedTimeFormat;
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
memoryCitationsMode?: MemoryCitationsMode;
|
||||
promptContribution?: ProviderSystemPromptContribution;
|
||||
}): string {
|
||||
return buildAgentSystemPrompt({
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -79,6 +81,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
userTimeFormat: params.userTimeFormat,
|
||||
contextFiles: params.contextFiles,
|
||||
memoryCitationsMode: params.memoryCitationsMode,
|
||||
promptContribution: params.promptContribution,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
28
src/agents/system-prompt-contribution.ts
Normal file
28
src/agents/system-prompt-contribution.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type ProviderSystemPromptSectionId =
|
||||
| "interaction_style"
|
||||
| "tool_call_style"
|
||||
| "execution_bias";
|
||||
|
||||
export type ProviderSystemPromptContribution = {
|
||||
/**
|
||||
* Cache-stable provider guidance inserted above the system-prompt cache boundary.
|
||||
*
|
||||
* Use this for static provider/model-family instructions that should preserve
|
||||
* KV cache reuse across turns.
|
||||
*/
|
||||
stablePrefix?: string;
|
||||
/**
|
||||
* Provider guidance inserted below the cache boundary.
|
||||
*
|
||||
* Use this only for genuinely dynamic text that is expected to vary across
|
||||
* runs or sessions.
|
||||
*/
|
||||
dynamicSuffix?: string;
|
||||
/**
|
||||
* Whole-section replacements for selected core prompt sections.
|
||||
*
|
||||
* Values should contain the complete rendered section, including any desired
|
||||
* heading such as `## Tool Call Style`.
|
||||
*/
|
||||
sectionOverrides?: Partial<Record<ProviderSystemPromptSectionId, string>>;
|
||||
};
|
||||
@@ -711,6 +711,56 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).not.toContain("# Dynamic Project Context");
|
||||
});
|
||||
|
||||
it("replaces provider-owned prompt sections without disturbing core ordering", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
promptContribution: {
|
||||
sectionOverrides: {
|
||||
interaction_style: "## Interaction Style\n\nCustom interaction guidance.",
|
||||
execution_bias: "## Execution Bias\n\nCustom execution guidance.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Interaction Style\n\nCustom interaction guidance.");
|
||||
expect(prompt).toContain("## Execution Bias\n\nCustom execution guidance.");
|
||||
expect(prompt).not.toContain("Bias toward action and momentum.");
|
||||
});
|
||||
|
||||
it("places provider stable prefixes above the cache boundary", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
promptContribution: {
|
||||
stablePrefix: "## Provider Stable Block\n\nStable provider guidance.",
|
||||
},
|
||||
});
|
||||
|
||||
const boundaryIndex = prompt.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
||||
const stableIndex = prompt.indexOf("## Provider Stable Block");
|
||||
const safetyIndex = prompt.indexOf("## Safety");
|
||||
|
||||
expect(stableIndex).toBeGreaterThan(-1);
|
||||
expect(boundaryIndex).toBeGreaterThan(stableIndex);
|
||||
expect(safetyIndex).toBeGreaterThan(stableIndex);
|
||||
});
|
||||
|
||||
it("places provider dynamic suffixes below the cache boundary", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
promptContribution: {
|
||||
dynamicSuffix: "## Provider Dynamic Block\n\nPer-turn provider guidance.",
|
||||
},
|
||||
});
|
||||
|
||||
const boundaryIndex = prompt.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
||||
const dynamicIndex = prompt.indexOf("## Provider Dynamic Block");
|
||||
const heartbeatIndex = prompt.indexOf("## Heartbeats");
|
||||
|
||||
expect(boundaryIndex).toBeGreaterThan(-1);
|
||||
expect(dynamicIndex).toBeGreaterThan(boundaryIndex);
|
||||
expect(heartbeatIndex).toBeGreaterThan(dynamicIndex);
|
||||
});
|
||||
|
||||
it("summarizes the message tool when available", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
} from "./prompt-cache-stability.js";
|
||||
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js";
|
||||
import type {
|
||||
ProviderSystemPromptContribution,
|
||||
ProviderSystemPromptSectionId,
|
||||
} from "./system-prompt-contribution.js";
|
||||
|
||||
/**
|
||||
* Controls which hardcoded sections are included in the system prompt.
|
||||
@@ -269,6 +273,25 @@ function buildExecutionBiasSection(params: { isMinimal: boolean }) {
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeProviderPromptBlock(value?: string): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeStructuredPromptSection(value);
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function buildOverridablePromptSection(params: {
|
||||
override?: string;
|
||||
fallback: string[];
|
||||
}): string[] {
|
||||
const override = normalizeProviderPromptBlock(params.override);
|
||||
if (override) {
|
||||
return [override, ""];
|
||||
}
|
||||
return params.fallback;
|
||||
}
|
||||
|
||||
function buildExecApprovalPromptGuidance(params: {
|
||||
runtimeChannel?: string;
|
||||
inlineButtonsEnabled?: boolean;
|
||||
@@ -332,6 +355,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
channel: string;
|
||||
};
|
||||
memoryCitationsMode?: MemoryCitationsMode;
|
||||
promptContribution?: ProviderSystemPromptContribution;
|
||||
}) {
|
||||
const acpEnabled = params.acpEnabled !== false;
|
||||
const sandboxedRuntime = params.sandboxInfo?.enabled === true;
|
||||
@@ -362,6 +386,17 @@ export function buildAgentSystemPrompt(params: {
|
||||
typeof params.extraSystemPrompt === "string"
|
||||
? normalizeStructuredPromptSection(params.extraSystemPrompt)
|
||||
: undefined;
|
||||
const promptContribution = params.promptContribution;
|
||||
const providerStablePrefix = normalizeProviderPromptBlock(promptContribution?.stablePrefix);
|
||||
const providerDynamicSuffix = normalizeProviderPromptBlock(promptContribution?.dynamicSuffix);
|
||||
const providerSectionOverrides = Object.fromEntries(
|
||||
Object.entries(promptContribution?.sectionOverrides ?? {})
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
normalizeProviderPromptBlock(typeof value === "string" ? value : undefined),
|
||||
])
|
||||
.filter(([, value]) => Boolean(value)),
|
||||
) as Partial<Record<ProviderSystemPromptSectionId, string>>;
|
||||
const ownerDisplay = params.ownerDisplay === "hash" ? "hash" : "raw";
|
||||
const ownerLine = buildOwnerIdentityLine(
|
||||
params.ownerNumbers ?? [],
|
||||
@@ -476,22 +511,38 @@ 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).",
|
||||
"",
|
||||
"## Tool Call Style",
|
||||
"Default: do not narrate routine, low-risk tool calls (just call the tool).",
|
||||
"Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
|
||||
"Keep narration brief and value-dense; avoid repeating obvious steps.",
|
||||
"Use plain human language for narration unless in a technical context.",
|
||||
"When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.",
|
||||
buildExecApprovalPromptGuidance({
|
||||
runtimeChannel: params.runtimeInfo?.channel,
|
||||
inlineButtonsEnabled,
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.interaction_style,
|
||||
fallback: [],
|
||||
}),
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",
|
||||
"When approvals are required, preserve and show the full command/script exactly as provided (including chained operators like &&, ||, |, ;, or multiline shells) so the user can approve what will actually run.",
|
||||
"",
|
||||
...buildExecutionBiasSection({
|
||||
isMinimal,
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.tool_call_style,
|
||||
fallback: [
|
||||
"## Tool Call Style",
|
||||
"Default: do not narrate routine, low-risk tool calls (just call the tool).",
|
||||
"Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
|
||||
"Keep narration brief and value-dense; avoid repeating obvious steps.",
|
||||
"Use plain human language for narration unless in a technical context.",
|
||||
"When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.",
|
||||
buildExecApprovalPromptGuidance({
|
||||
runtimeChannel: params.runtimeInfo?.channel,
|
||||
inlineButtonsEnabled,
|
||||
}),
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",
|
||||
"When approvals are required, preserve and show the full command/script exactly as provided (including chained operators like &&, ||, |, ;, or multiline shells) so the user can approve what will actually run.",
|
||||
"",
|
||||
],
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerSectionOverrides.execution_bias,
|
||||
fallback: buildExecutionBiasSection({
|
||||
isMinimal,
|
||||
}),
|
||||
}),
|
||||
...buildOverridablePromptSection({
|
||||
override: providerStablePrefix,
|
||||
fallback: [],
|
||||
}),
|
||||
...safetySection,
|
||||
"## OpenClaw CLI Quick Reference",
|
||||
@@ -682,6 +733,9 @@ export function buildAgentSystemPrompt(params: {
|
||||
promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context";
|
||||
lines.push(contextHeader, extraSystemPrompt, "");
|
||||
}
|
||||
if (providerDynamicSuffix) {
|
||||
lines.push(providerDynamicSuffix, "");
|
||||
}
|
||||
|
||||
// Skip heartbeats for subagent/none modes
|
||||
if (!isMinimal && heartbeatPrompt) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelProviderConfig } from "../config/types.js";
|
||||
import { resolveCatalogHookProviderPluginIds } from "./providers.js";
|
||||
@@ -41,6 +42,7 @@ import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderResolveTransportTurnStateContext,
|
||||
ProviderResolveWebSocketSessionPolicyContext,
|
||||
ProviderSystemPromptContributionContext,
|
||||
ProviderRuntimeModel,
|
||||
ProviderThinkingPolicyContext,
|
||||
ProviderTransportTurnState,
|
||||
@@ -208,6 +210,19 @@ export function runProviderDynamicModel(params: {
|
||||
return resolveProviderRuntimePlugin(params)?.resolveDynamicModel?.(params.context) ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderSystemPromptContribution(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderSystemPromptContributionContext;
|
||||
}): ProviderSystemPromptContribution | undefined {
|
||||
return (
|
||||
resolveProviderRuntimePlugin(params)?.resolveSystemPromptContribution?.(params.context) ??
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function prepareProviderDynamicModel(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import type { FailoverReason } from "../agents/pi-embedded-helpers/types.js";
|
||||
import type { ProviderRequestTransportOverrides } from "../agents/provider-request-config.js";
|
||||
import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js";
|
||||
import type { PromptMode } from "../agents/system-prompt.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
@@ -1044,6 +1046,18 @@ export type ProviderDeferSyntheticProfileAuthContext = {
|
||||
resolvedApiKey?: string;
|
||||
};
|
||||
|
||||
export type ProviderSystemPromptContributionContext = {
|
||||
config?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
promptMode: PromptMode;
|
||||
runtimeChannel?: string;
|
||||
runtimeCapabilities?: string[];
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
/** Text-inference provider capability registered by a plugin. */
|
||||
export type ProviderPlugin = {
|
||||
id: string;
|
||||
@@ -1401,6 +1415,15 @@ export type ProviderPlugin = {
|
||||
resolveDefaultThinkingLevel?: (
|
||||
ctx: ProviderDefaultThinkingPolicyContext,
|
||||
) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined;
|
||||
/**
|
||||
* Provider-owned system-prompt contribution.
|
||||
*
|
||||
* Use this when a provider/model family needs cache-aware prompt tuning
|
||||
* without replacing the full OpenClaw-owned system prompt.
|
||||
*/
|
||||
resolveSystemPromptContribution?: (
|
||||
ctx: ProviderSystemPromptContributionContext,
|
||||
) => ProviderSystemPromptContribution | null | undefined;
|
||||
/**
|
||||
* Provider-owned global config defaults.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user