hooks: add system-context prompt composition coverage

This commit is contained in:
Gustavo Madeira Santana
2026-03-05 11:15:01 -05:00
parent 7f58409e30
commit e6fad2030f
4 changed files with 55 additions and 14 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Slack/DM typing feedback: add `channels.slack.typingReaction` so Socket Mode DMs can show reaction-based processing status even when Slack native assistant typing is unavailable. (#19816) Thanks @dalefrieswthat.
- Cron/job snapshot persistence: skip backup during normalization persistence in `ensureLoaded` so `jobs.json.bak` keeps the pre-edit snapshot for recovery, while preserving backup creation on explicit user-driven writes. (#35234) Thanks @0xsline.
- TTS/OpenAI-compatible endpoints: add `messages.tts.openai.baseUrl` config support with config-over-env precedence, endpoint-aware directive validation, and OpenAI TTS request routing to the resolved base URL. (#34321) thanks @RealKai42.
- Plugins/before_prompt_build system-context fields: add `prependSystemContext` and `appendSystemContext` so static plugin guidance can be placed in system prompt space for provider caching and lower repeated prompt token cost. (#35177) thanks @maweibin.
### Fixes

View File

@@ -82,7 +82,7 @@ See [Hooks](/automation/hooks) for setup and examples.
These run inside the agent loop or gateway pipeline:
- **`before_model_resolve`**: runs pre-session (no `messages`) to deterministically override provider/model before model resolution.
- **`before_prompt_build`**: runs after session load (with `messages`) to inject `prependContext`/`systemPrompt` before prompt submission.
- **`before_prompt_build`**: runs after session load (with `messages`) to inject `prependContext`, `systemPrompt`, `prependSystemContext`, or `appendSystemContext` before prompt submission.
- **`before_agent_start`**: legacy compatibility hook that may run in either phase; prefer the explicit hooks above.
- **`agent_end`**: inspect the final message list and run metadata after completion.
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import {
composeSystemPromptWithHookContext,
isOllamaCompatProvider,
resolveAttemptFsWorkspaceOnly,
resolveOllamaBaseUrlForRun,
@@ -75,6 +76,31 @@ describe("resolvePromptBuildHookResult", () => {
});
});
describe("composeSystemPromptWithHookContext", () => {
it("returns undefined when no hook system context is provided", () => {
expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined();
});
it("builds prepend/base/append system prompt order", () => {
expect(
composeSystemPromptWithHookContext({
baseSystemPrompt: " base system ",
prependSystemContext: " prepend ",
appendSystemContext: " append ",
}),
).toBe("prepend\n\nbase system\n\nappend");
});
it("avoids blank separators when base system prompt is empty", () => {
expect(
composeSystemPromptWithHookContext({
baseSystemPrompt: " ",
appendSystemContext: " append only ",
}),
).toBe("append only");
});
});
describe("resolvePromptModeForSession", () => {
it("uses minimal mode for subagent sessions", () => {
expect(resolvePromptModeForSession("agent:main:subagent:child")).toBe("minimal");

View File

@@ -582,6 +582,22 @@ export async function resolvePromptBuildHookResult(params: {
};
}
export function composeSystemPromptWithHookContext(params: {
baseSystemPrompt?: string;
prependSystemContext?: string;
appendSystemContext?: string;
}): string | undefined {
const prependSystem = params.prependSystemContext?.trim();
const appendSystem = params.appendSystemContext?.trim();
const baseSystem = params.baseSystemPrompt?.trim() ?? "";
if (!prependSystem && !appendSystem) {
return undefined;
}
return [prependSystem, baseSystem, appendSystem]
.filter((value): value is string => Boolean(value))
.join("\n\n");
}
export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "full" {
if (!sessionKey) {
return "full";
@@ -1531,20 +1547,18 @@ export async function runEmbeddedAttempt(
systemPromptText = legacySystemPrompt;
log.debug(`hooks: applied systemPrompt override (${legacySystemPrompt.length} chars)`);
}
const prependSystem = hookResult?.prependSystemContext?.trim();
const appendSystem = hookResult?.appendSystemContext?.trim();
if (prependSystem || appendSystem) {
let base = systemPromptText ?? "";
if (prependSystem) {
base = `${prependSystem}\n\n${base}`;
}
if (appendSystem) {
base = `${base}\n\n${appendSystem}`;
}
applySystemPromptOverrideToSession(activeSession, base);
systemPromptText = base;
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
baseSystemPrompt: systemPromptText,
prependSystemContext: hookResult?.prependSystemContext,
appendSystemContext: hookResult?.appendSystemContext,
});
if (prependedOrAppendedSystemPrompt) {
const prependSystemLen = hookResult?.prependSystemContext?.trim().length ?? 0;
const appendSystemLen = hookResult?.appendSystemContext?.trim().length ?? 0;
applySystemPromptOverrideToSession(activeSession, prependedOrAppendedSystemPrompt);
systemPromptText = prependedOrAppendedSystemPrompt;
log.debug(
`hooks: applied prependSystemContext/appendSystemContext (${prependSystem?.length ?? 0}+${appendSystem?.length ?? 0} chars)`,
`hooks: applied prependSystemContext/appendSystemContext (${prependSystemLen}+${appendSystemLen} chars)`,
);
}
}