Skip to content

Commit 930852a

Browse files
authored
feat(agents): support per-agent bootstrap profiles
Summary:\n- Add optional per-agent bootstrap profile overrides for contextInjection, bootstrapMaxChars, and bootstrapTotalMaxChars.\n- Resolve per-agent bootstrap profile settings before agents.defaults and thread the resolved session agent through embedded, compact, CLI, and /context diagnostic paths.\n- Update schema/help/docs/changelog plus focused runtime, schema, and /context regression coverage.\n\nVerification:\n- Local focused auto-reply tests and formatter checks passed.\n- Local pnpm check:changed passed before landing follow-ups.\n- Local Node 24 pnpm check:test-types passed after merging latest main into the PR branch.\n- GitHub PR state CLEAN at 0ff1206.\n- ClawSweeper re-review completed successfully with no actionable repair finding.\n\nFixes #69966.
1 parent 64d4f99 commit 930852a

19 files changed

Lines changed: 241 additions & 22 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- Agents/config: support per-agent bootstrap profile overrides for `contextInjection`, `bootstrapMaxChars`, and `bootstrapTotalMaxChars`, inheriting from `agents.defaults` when omitted. Fixes #69966. Thanks @BunsDev.
910
- Dependencies: route root ambient Node proxy agents through `@openclaw/proxyline` and drop root `proxy-agent`, `https-proxy-agent`, and `minimatch` dependencies.
1011
- Canvas: lazy-load HTTP host, hosted media resolver, CLI implementation, and tool runtime modules so Gateway startup only pays Canvas implementation cost on first use. (#82001) Thanks @samzong.
1112
- Control UI/i18n: add a `pnpm ui:i18n:report` baseline report for hardcoded-copy focus areas and locale fallback metadata. (#81320) Thanks @samzong.

docs/gateway/config-agents.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ Controls when workspace bootstrap files are injected into the system prompt. Def
9494
}
9595
```
9696

97+
Per-agent override: `agents.list[].contextInjection`. Omitted values inherit
98+
`agents.defaults.contextInjection`.
99+
97100
### `agents.defaults.bootstrapMaxChars`
98101

99102
Max characters per workspace bootstrap file before truncation. Default: `12000`.
@@ -104,6 +107,9 @@ Max characters per workspace bootstrap file before truncation. Default: `12000`.
104107
}
105108
```
106109

110+
Per-agent override: `agents.list[].bootstrapMaxChars`. Omitted values inherit
111+
`agents.defaults.bootstrapMaxChars`.
112+
107113
### `agents.defaults.bootstrapTotalMaxChars`
108114

109115
Max total characters injected across all workspace bootstrap files. Default: `60000`.
@@ -114,6 +120,35 @@ Max total characters injected across all workspace bootstrap files. Default: `60
114120
}
115121
```
116122

123+
Per-agent override: `agents.list[].bootstrapTotalMaxChars`. Omitted values
124+
inherit `agents.defaults.bootstrapTotalMaxChars`.
125+
126+
### Per-agent bootstrap profile overrides
127+
128+
Use per-agent bootstrap profile overrides when one agent needs different prompt
129+
injection behavior from the shared defaults. Omitted fields inherit from
130+
`agents.defaults`.
131+
132+
```json5
133+
{
134+
agents: {
135+
defaults: {
136+
contextInjection: "continuation-skip",
137+
bootstrapMaxChars: 12000,
138+
bootstrapTotalMaxChars: 60000,
139+
},
140+
list: [
141+
{
142+
id: "strict-worker",
143+
contextInjection: "always",
144+
bootstrapMaxChars: 50000,
145+
bootstrapTotalMaxChars: 300000,
146+
},
147+
],
148+
},
149+
}
150+
```
151+
117152
### `agents.defaults.bootstrapPromptTruncationWarning`
118153

119154
Controls the agent-visible system-prompt notice when bootstrap context is truncated.
@@ -157,6 +192,9 @@ Use the matching per-agent override only when one agent needs a different
157192
budget:
158193

159194
- `agents.list[].skillsLimits.maxSkillsPromptChars`
195+
- `agents.list[].contextInjection`
196+
- `agents.list[].bootstrapMaxChars`
197+
- `agents.list[].bootstrapTotalMaxChars`
160198
- `agents.list[].contextLimits.*`
161199

162200
#### `agents.defaults.startupContext`

src/agents/agent-scope-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export type ResolvedAgentConfig = {
2222
verboseDefault?: AgentDefaultsConfig["verboseDefault"];
2323
reasoningDefault?: AgentEntry["reasoningDefault"];
2424
fastModeDefault?: AgentEntry["fastModeDefault"];
25+
contextInjection?: AgentEntry["contextInjection"];
26+
bootstrapMaxChars?: AgentEntry["bootstrapMaxChars"];
27+
bootstrapTotalMaxChars?: AgentEntry["bootstrapTotalMaxChars"];
2528
skills?: AgentEntry["skills"];
2629
memorySearch?: AgentEntry["memorySearch"];
2730
humanDelay?: AgentEntry["humanDelay"];
@@ -122,6 +125,9 @@ export function resolveAgentConfig(
122125
verboseDefault: entry.verboseDefault ?? agentDefaults?.verboseDefault,
123126
reasoningDefault: entry.reasoningDefault,
124127
fastModeDefault: entry.fastModeDefault,
128+
contextInjection: entry.contextInjection,
129+
bootstrapMaxChars: entry.bootstrapMaxChars,
130+
bootstrapTotalMaxChars: entry.bootstrapTotalMaxChars,
125131
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
126132
memorySearch: entry.memorySearch,
127133
humanDelay: entry.humanDelay,

src/agents/bootstrap-files.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,4 +512,32 @@ describe("resolveContextInjectionMode", () => {
512512
} as never),
513513
).toBe("continuation-skip");
514514
});
515+
516+
it("uses per-agent contextInjection before defaults", () => {
517+
expect(
518+
resolveContextInjectionMode(
519+
{
520+
agents: {
521+
defaults: { contextInjection: "continuation-skip" },
522+
list: [{ id: "strict", contextInjection: "always" }],
523+
},
524+
} as never,
525+
"strict",
526+
),
527+
).toBe("always");
528+
});
529+
530+
it("falls back to defaults when the agent has no contextInjection override", () => {
531+
expect(
532+
resolveContextInjectionMode(
533+
{
534+
agents: {
535+
defaults: { contextInjection: "never" },
536+
list: [{ id: "worker" }],
537+
},
538+
} as never,
539+
"worker",
540+
),
541+
).toBe("never");
542+
});
515543
});

src/agents/bootstrap-files.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from "node:path";
33
import type { AgentContextInjection } from "../config/types.agent-defaults.js";
44
import type { OpenClawConfig } from "../config/types.openclaw.js";
55
import { normalizeOptionalString } from "../shared/string-coerce.js";
6-
import { resolveSessionAgentIds } from "./agent-scope.js";
6+
import { resolveAgentConfig, resolveSessionAgentIds } from "./agent-scope.js";
77
import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
88
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
99
import { shouldIncludeHeartbeatGuidanceForSystemPrompt } from "./heartbeat-system-prompt.js";
@@ -51,7 +51,15 @@ export function _resetBootstrapWarningCacheForTest(): void {
5151
bootstrapWarningOrder.length = 0;
5252
}
5353

54-
export function resolveContextInjectionMode(config?: OpenClawConfig): AgentContextInjection {
54+
export function resolveContextInjectionMode(
55+
config?: OpenClawConfig,
56+
agentId?: string | null,
57+
): AgentContextInjection {
58+
const agentMode =
59+
config && agentId ? resolveAgentConfig(config, agentId)?.contextInjection : undefined;
60+
if (agentMode === "always" || agentMode === "continuation-skip" || agentMode === "never") {
61+
return agentMode;
62+
}
5563
return config?.agents?.defaults?.contextInjection ?? "always";
5664
}
5765

@@ -287,12 +295,13 @@ export function buildBootstrapContextForFiles(
287295
bootstrapFiles: WorkspaceBootstrapFile[],
288296
params: {
289297
config?: OpenClawConfig;
298+
agentId?: string | null;
290299
warn?: (message: string) => void;
291300
},
292301
): EmbeddedContextFile[] {
293302
const contextFiles = buildBootstrapContextFiles(bootstrapFiles, {
294-
maxChars: resolveBootstrapMaxChars(params.config),
295-
totalMaxChars: resolveBootstrapTotalMaxChars(params.config),
303+
maxChars: resolveBootstrapMaxChars(params.config, params.agentId),
304+
totalMaxChars: resolveBootstrapTotalMaxChars(params.config, params.agentId),
296305
warn: params.warn,
297306
});
298307
return contextFiles;

src/agents/cli-runner/prepare.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export async function prepareCliRunContext(
171171
config: params.config,
172172
sessionKey: params.sessionKey,
173173
sessionId: params.sessionId,
174+
agentId: sessionAgentId,
174175
contextMode: params.bootstrapContextMode,
175176
runKind: params.bootstrapContextRunKind,
176177
warn: prepareDeps.makeBootstrapWarn({
@@ -179,8 +180,8 @@ export async function prepareCliRunContext(
179180
warn: (message) => cliBackendLog.warn(message),
180181
}),
181182
});
182-
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config);
183-
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config);
183+
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config, sessionAgentId);
184+
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config, sessionAgentId);
184185
const bootstrapAnalysis = analyzeBootstrapBudget({
185186
files: buildBootstrapInjectionStats({
186187
bootstrapFiles,

src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ describe("buildBootstrapContextFiles", () => {
225225

226226
type BootstrapLimitResolverCase = {
227227
name: "bootstrapMaxChars" | "bootstrapTotalMaxChars";
228-
resolve: (cfg?: OpenClawConfig) => number;
228+
resolve: (cfg?: OpenClawConfig, agentId?: string | null) => number;
229229
defaultValue: number;
230230
};
231231

@@ -258,6 +258,30 @@ describe("bootstrap limit resolvers", () => {
258258
}
259259
});
260260

261+
it("uses per-agent values before defaults", () => {
262+
for (const resolver of BOOTSTRAP_LIMIT_RESOLVERS) {
263+
const cfg = {
264+
agents: {
265+
defaults: { [resolver.name]: 12345 },
266+
list: [{ id: "worker", [resolver.name]: 6789 }],
267+
},
268+
} as OpenClawConfig;
269+
expect(resolver.resolve(cfg, "worker")).toBe(6789);
270+
}
271+
});
272+
273+
it("falls back to defaults when the agent has no override", () => {
274+
for (const resolver of BOOTSTRAP_LIMIT_RESOLVERS) {
275+
const cfg = {
276+
agents: {
277+
defaults: { [resolver.name]: 12345 },
278+
list: [{ id: "worker" }],
279+
},
280+
} as OpenClawConfig;
281+
expect(resolver.resolve(cfg, "worker")).toBe(12345);
282+
}
283+
});
284+
261285
it("fall back when values are invalid", () => {
262286
for (const resolver of BOOTSTRAP_LIMIT_RESOLVERS) {
263287
const cfg = {

src/agents/pi-embedded-helpers/bootstrap.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
55
import { sanitizeGoogleAssistantFirstOrdering } from "../../shared/google-turn-ordering.js";
66
import { normalizeOptionalString } from "../../shared/string-coerce.js";
77
import { truncateUtf16Safe } from "../../utils.js";
8+
import { resolveAgentConfig } from "../agent-scope.js";
89
import type { WorkspaceBootstrapFile } from "../workspace.js";
910
import type { EmbeddedContextFile } from "./types.js";
1011

@@ -103,16 +104,27 @@ type TrimBootstrapResult = {
103104
originalLength: number;
104105
};
105106

106-
export function resolveBootstrapMaxChars(cfg?: OpenClawConfig): number {
107-
const raw = cfg?.agents?.defaults?.bootstrapMaxChars;
107+
export function resolveBootstrapMaxChars(cfg?: OpenClawConfig, agentId?: string | null): number {
108+
const raw =
109+
cfg && agentId
110+
? (resolveAgentConfig(cfg, agentId)?.bootstrapMaxChars ??
111+
cfg.agents?.defaults?.bootstrapMaxChars)
112+
: cfg?.agents?.defaults?.bootstrapMaxChars;
108113
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
109114
return Math.floor(raw);
110115
}
111116
return DEFAULT_BOOTSTRAP_MAX_CHARS;
112117
}
113118

114-
export function resolveBootstrapTotalMaxChars(cfg?: OpenClawConfig): number {
115-
const raw = cfg?.agents?.defaults?.bootstrapTotalMaxChars;
119+
export function resolveBootstrapTotalMaxChars(
120+
cfg?: OpenClawConfig,
121+
agentId?: string | null,
122+
): number {
123+
const raw =
124+
cfg && agentId
125+
? (resolveAgentConfig(cfg, agentId)?.bootstrapTotalMaxChars ??
126+
cfg.agents?.defaults?.bootstrapTotalMaxChars)
127+
: cfg?.agents?.defaults?.bootstrapTotalMaxChars;
116128
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
117129
return Math.floor(raw);
118130
}

src/agents/pi-embedded-runner/compact.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ async function compactEmbeddedPiSessionDirectOnce(
631631

632632
const sessionLabel = params.sessionKey ?? params.sessionId;
633633
const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
634-
const contextInjectionMode = resolveContextInjectionMode(params.config);
634+
const contextInjectionMode = resolveContextInjectionMode(params.config, effectiveSkillAgentId);
635635
const { contextFiles } =
636636
contextInjectionMode === "never"
637637
? { contextFiles: [] }
@@ -640,6 +640,7 @@ async function compactEmbeddedPiSessionDirectOnce(
640640
config: params.config,
641641
sessionKey: params.sessionKey,
642642
sessionId: params.sessionId,
643+
agentId: effectiveSkillAgentId,
643644
warn: makeBootstrapWarn({
644645
sessionLabel,
645646
warn: (message) => log.warn(message),

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ export async function runEmbeddedAttempt(
940940
prepStages.mark("skills");
941941

942942
const sessionLabel = params.sessionKey ?? params.sessionId;
943-
const contextInjectionMode = resolveContextInjectionMode(params.config);
943+
const contextInjectionMode = resolveContextInjectionMode(params.config, sessionAgentId);
944944
const isRawModelRun = params.modelRun === true || params.promptMode === "none";
945945
if (isRawModelRun && log.isEnabled("debug")) {
946946
log.debug(
@@ -1194,6 +1194,7 @@ export async function runEmbeddedAttempt(
11941194
config: params.config,
11951195
sessionKey: params.sessionKey,
11961196
sessionId: params.sessionId,
1197+
agentId: sessionAgentId,
11971198
warn: bootstrapWarn,
11981199
contextMode: params.bootstrapContextMode,
11991200
runKind: params.bootstrapContextRunKind,
@@ -1202,6 +1203,7 @@ export async function runEmbeddedAttempt(
12021203
bootstrapFiles,
12031204
contextFiles: buildBootstrapContextForFiles(bootstrapFiles, {
12041205
config: params.config,
1206+
agentId: sessionAgentId,
12051207
warn: bootstrapWarn,
12061208
}),
12071209
};
@@ -1219,8 +1221,8 @@ export async function runEmbeddedAttempt(
12191221
const bootstrapFilesForInjectionStats = bootstrapRouting.includeBootstrapInSystemContext
12201222
? hookAdjustedBootstrapFiles
12211223
: hookAdjustedBootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME);
1222-
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config);
1223-
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config);
1224+
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config, sessionAgentId);
1225+
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config, sessionAgentId);
12241226
const bootstrapAnalysis = analyzeBootstrapBudget({
12251227
files: buildBootstrapInjectionStats({
12261228
bootstrapFiles: bootstrapFilesForInjectionStats,

0 commit comments

Comments
 (0)