mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
Agents: make bootstrap prompt caps opt-in by default
This commit is contained in:
@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
|
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
|
||||||
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
|
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
|
||||||
- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code.
|
- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code.
|
||||||
|
- Agents/Context: make bootstrap prompt limits opt-in (`bootstrapMaxChars`/`bootstrapTotalMaxChars`), preserve full core bootstrap content by default, and surface both per-file and total bootstrap caps in `/context` reports.
|
||||||
- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
|
- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
|
||||||
- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
|
- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
|
||||||
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
|
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ See [Memory](/concepts/memory) for the workflow and automatic memory flush.
|
|||||||
|
|
||||||
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into
|
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into
|
||||||
the session and continues. Large bootstrap files are truncated when injected;
|
the session and continues. Large bootstrap files are truncated when injected;
|
||||||
adjust the limit with `agents.defaults.bootstrapMaxChars` (default: 20000).
|
adjust limits with `agents.defaults.bootstrapMaxChars` and/or
|
||||||
|
`agents.defaults.bootstrapTotalMaxChars` (unset = unlimited).
|
||||||
`openclaw setup` can recreate missing defaults without overwriting existing
|
`openclaw setup` can recreate missing defaults without overwriting existing
|
||||||
files.
|
files.
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ By default, OpenClaw injects a fixed set of workspace files (if present):
|
|||||||
- `HEARTBEAT.md`
|
- `HEARTBEAT.md`
|
||||||
- `BOOTSTRAP.md` (first-run only)
|
- `BOOTSTRAP.md` (first-run only)
|
||||||
|
|
||||||
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `24000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
|
Large files are truncated only when limits are configured. Use
|
||||||
|
`agents.defaults.bootstrapMaxChars` for per-file limits and
|
||||||
|
`agents.defaults.bootstrapTotalMaxChars` for total bootstrap limits (unset =
|
||||||
|
unlimited). `/context` shows **raw vs injected** sizes and whether truncation
|
||||||
|
happened.
|
||||||
|
|
||||||
## Skills: what’s injected vs loaded on-demand
|
## Skills: what’s injected vs loaded on-demand
|
||||||
|
|
||||||
|
|||||||
@@ -70,10 +70,11 @@ compaction.
|
|||||||
> are accessed on demand via the `memory_search` and `memory_get` tools, so they
|
> are accessed on demand via the `memory_search` and `memory_get` tools, so they
|
||||||
> do not count against the context window unless the model explicitly reads them.
|
> do not count against the context window unless the model explicitly reads them.
|
||||||
|
|
||||||
Large files are truncated with a marker. The max per-file size is controlled by
|
Large files are truncated with a marker only when limits are configured. Use
|
||||||
`agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap
|
`agents.defaults.bootstrapMaxChars` for a per-file cap and
|
||||||
content across files is capped by `agents.defaults.bootstrapTotalMaxChars`
|
`agents.defaults.bootstrapTotalMaxChars` for a total cap across all injected
|
||||||
(default: 24000). Missing files inject a short missing-file marker.
|
bootstrap files. If both are unset, bootstrap injection is unlimited. Missing
|
||||||
|
files inject a short missing-file marker.
|
||||||
|
|
||||||
Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files
|
Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files
|
||||||
are filtered out to keep the sub-agent context small).
|
are filtered out to keep the sub-agent context small).
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md`
|
|||||||
|
|
||||||
### `agents.defaults.bootstrapMaxChars`
|
### `agents.defaults.bootstrapMaxChars`
|
||||||
|
|
||||||
Max characters per workspace bootstrap file before truncation. Default: `20000`.
|
Optional max characters per workspace bootstrap file before truncation. If unset, per-file bootstrap injection is unlimited.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -589,7 +589,7 @@ Max characters per workspace bootstrap file before truncation. Default: `20000`.
|
|||||||
|
|
||||||
### `agents.defaults.bootstrapTotalMaxChars`
|
### `agents.defaults.bootstrapTotalMaxChars`
|
||||||
|
|
||||||
Max total characters injected across all workspace bootstrap files. Default: `24000`.
|
Optional max total characters injected across all workspace bootstrap files. If unset, total bootstrap injection is unlimited.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ OpenClaw assembles its own system prompt on every run. It includes:
|
|||||||
- Tool list + short descriptions
|
- Tool list + short descriptions
|
||||||
- Skills list (only metadata; instructions are loaded on demand with `read`)
|
- Skills list (only metadata; instructions are loaded on demand with `read`)
|
||||||
- Self-update instructions
|
- Self-update instructions
|
||||||
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` and/or `memory.md` when present). Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 20000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 24000). `memory/*.md` files are on-demand via memory tools and are not auto-injected.
|
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` and/or `memory.md` when present). Per-file and total bootstrap truncation only apply when configured via `agents.defaults.bootstrapMaxChars` and/or `agents.defaults.bootstrapTotalMaxChars` (unset = unlimited). `memory/*.md` files are on-demand via memory tools and are not auto-injected.
|
||||||
- Time (UTC + user timezone)
|
- Time (UTC + user timezone)
|
||||||
- Reply tags + heartbeat behavior
|
- Reply tags + heartbeat behavior
|
||||||
- Runtime metadata (host/OS/model/thinking)
|
- Runtime metadata (host/OS/model/thinking)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { describe, expect, it } from "vitest";
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
DEFAULT_BOOTSTRAP_MAX_CHARS,
|
|
||||||
DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
|
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
resolveBootstrapTotalMaxChars,
|
resolveBootstrapTotalMaxChars,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
@@ -50,15 +48,15 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
expect(warnings[0]).toContain("TOOLS.md");
|
expect(warnings[0]).toContain("TOOLS.md");
|
||||||
expect(warnings[0]).toContain("limit 200");
|
expect(warnings[0]).toContain("limit 200");
|
||||||
});
|
});
|
||||||
it("keeps content under the default limit", () => {
|
it("does not truncate bootstrap content by default", () => {
|
||||||
const long = "a".repeat(DEFAULT_BOOTSTRAP_MAX_CHARS - 10);
|
const long = "a".repeat(30_000);
|
||||||
const files = [makeFile({ content: long })];
|
const files = [makeFile({ content: long })];
|
||||||
const [result] = buildBootstrapContextFiles(files);
|
const [result] = buildBootstrapContextFiles(files);
|
||||||
expect(result?.content).toBe(long);
|
expect(result?.content).toBe(long);
|
||||||
expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]");
|
expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("caps total injected bootstrap characters across files", () => {
|
it("does not cap total injected bootstrap characters by default", () => {
|
||||||
const files = [
|
const files = [
|
||||||
makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }),
|
makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }),
|
||||||
makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }),
|
makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }),
|
||||||
@@ -66,7 +64,20 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
];
|
];
|
||||||
const result = buildBootstrapContextFiles(files);
|
const result = buildBootstrapContextFiles(files);
|
||||||
const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0);
|
const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0);
|
||||||
expect(totalChars).toBeLessThanOrEqual(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS);
|
expect(totalChars).toBe(30_000);
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[2]?.content).toBe("c".repeat(10_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps total injected bootstrap characters when totalMaxChars is configured", () => {
|
||||||
|
const files = [
|
||||||
|
makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }),
|
||||||
|
makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }),
|
||||||
|
makeFile({ name: "USER.md", path: "/tmp/USER.md", content: "c".repeat(10_000) }),
|
||||||
|
];
|
||||||
|
const result = buildBootstrapContextFiles(files, { totalMaxChars: 24_000 });
|
||||||
|
const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0);
|
||||||
|
expect(totalChars).toBeLessThanOrEqual(24_000);
|
||||||
expect(result).toHaveLength(3);
|
expect(result).toHaveLength(3);
|
||||||
expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]");
|
expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]");
|
||||||
});
|
});
|
||||||
@@ -105,8 +116,8 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveBootstrapMaxChars", () => {
|
describe("resolveBootstrapMaxChars", () => {
|
||||||
it("returns default when unset", () => {
|
it("returns undefined when unset", () => {
|
||||||
expect(resolveBootstrapMaxChars()).toBe(DEFAULT_BOOTSTRAP_MAX_CHARS);
|
expect(resolveBootstrapMaxChars()).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("uses configured value when valid", () => {
|
it("uses configured value when valid", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -114,17 +125,17 @@ describe("resolveBootstrapMaxChars", () => {
|
|||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
expect(resolveBootstrapMaxChars(cfg)).toBe(12345);
|
expect(resolveBootstrapMaxChars(cfg)).toBe(12345);
|
||||||
});
|
});
|
||||||
it("falls back when invalid", () => {
|
it("returns undefined when invalid", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: { defaults: { bootstrapMaxChars: -1 } },
|
agents: { defaults: { bootstrapMaxChars: -1 } },
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
expect(resolveBootstrapMaxChars(cfg)).toBe(DEFAULT_BOOTSTRAP_MAX_CHARS);
|
expect(resolveBootstrapMaxChars(cfg)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveBootstrapTotalMaxChars", () => {
|
describe("resolveBootstrapTotalMaxChars", () => {
|
||||||
it("returns default when unset", () => {
|
it("returns undefined when unset", () => {
|
||||||
expect(resolveBootstrapTotalMaxChars()).toBe(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS);
|
expect(resolveBootstrapTotalMaxChars()).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("uses configured value when valid", () => {
|
it("uses configured value when valid", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -132,10 +143,10 @@ describe("resolveBootstrapTotalMaxChars", () => {
|
|||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
expect(resolveBootstrapTotalMaxChars(cfg)).toBe(12345);
|
expect(resolveBootstrapTotalMaxChars(cfg)).toBe(12345);
|
||||||
});
|
});
|
||||||
it("falls back when invalid", () => {
|
it("returns undefined when invalid", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: { defaults: { bootstrapTotalMaxChars: -1 } },
|
agents: { defaults: { bootstrapTotalMaxChars: -1 } },
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
expect(resolveBootstrapTotalMaxChars(cfg)).toBe(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS);
|
expect(resolveBootstrapTotalMaxChars(cfg)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
DEFAULT_BOOTSTRAP_MAX_CHARS,
|
|
||||||
DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
|
|
||||||
ensureSessionHeader,
|
ensureSessionHeader,
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
resolveBootstrapTotalMaxChars,
|
resolveBootstrapTotalMaxChars,
|
||||||
|
|||||||
@@ -82,8 +82,6 @@ export function stripThoughtSignatures<T>(
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000;
|
|
||||||
export const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 24_000;
|
|
||||||
const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64;
|
const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64;
|
||||||
const BOOTSTRAP_HEAD_RATIO = 0.7;
|
const BOOTSTRAP_HEAD_RATIO = 0.7;
|
||||||
const BOOTSTRAP_TAIL_RATIO = 0.2;
|
const BOOTSTRAP_TAIL_RATIO = 0.2;
|
||||||
@@ -91,32 +89,38 @@ const BOOTSTRAP_TAIL_RATIO = 0.2;
|
|||||||
type TrimBootstrapResult = {
|
type TrimBootstrapResult = {
|
||||||
content: string;
|
content: string;
|
||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
maxChars: number;
|
maxChars?: number;
|
||||||
originalLength: number;
|
originalLength: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveBootstrapMaxChars(cfg?: OpenClawConfig): number {
|
function resolveConfiguredBootstrapLimit(raw: unknown): number | undefined {
|
||||||
const raw = cfg?.agents?.defaults?.bootstrapMaxChars;
|
|
||||||
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
||||||
return Math.floor(raw);
|
return Math.floor(raw);
|
||||||
}
|
}
|
||||||
return DEFAULT_BOOTSTRAP_MAX_CHARS;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBootstrapTotalMaxChars(cfg?: OpenClawConfig): number {
|
export function resolveBootstrapMaxChars(cfg?: OpenClawConfig): number | undefined {
|
||||||
const raw = cfg?.agents?.defaults?.bootstrapTotalMaxChars;
|
return resolveConfiguredBootstrapLimit(cfg?.agents?.defaults?.bootstrapMaxChars);
|
||||||
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
}
|
||||||
return Math.floor(raw);
|
|
||||||
}
|
export function resolveBootstrapTotalMaxChars(cfg?: OpenClawConfig): number | undefined {
|
||||||
return DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS;
|
return resolveConfiguredBootstrapLimit(cfg?.agents?.defaults?.bootstrapTotalMaxChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimBootstrapContent(
|
function trimBootstrapContent(
|
||||||
content: string,
|
content: string,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
maxChars: number,
|
maxChars?: number,
|
||||||
): TrimBootstrapResult {
|
): TrimBootstrapResult {
|
||||||
const trimmed = content.trimEnd();
|
const trimmed = content.trimEnd();
|
||||||
|
if (typeof maxChars !== "number") {
|
||||||
|
return {
|
||||||
|
content: trimmed,
|
||||||
|
truncated: false,
|
||||||
|
originalLength: trimmed.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (trimmed.length <= maxChars) {
|
if (trimmed.length <= maxChars) {
|
||||||
return {
|
return {
|
||||||
content: trimmed,
|
content: trimmed,
|
||||||
@@ -188,48 +192,71 @@ export function buildBootstrapContextFiles(
|
|||||||
files: WorkspaceBootstrapFile[],
|
files: WorkspaceBootstrapFile[],
|
||||||
opts?: { warn?: (message: string) => void; maxChars?: number; totalMaxChars?: number },
|
opts?: { warn?: (message: string) => void; maxChars?: number; totalMaxChars?: number },
|
||||||
): EmbeddedContextFile[] {
|
): EmbeddedContextFile[] {
|
||||||
const maxChars = opts?.maxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS;
|
const maxChars = resolveConfiguredBootstrapLimit(opts?.maxChars);
|
||||||
const totalMaxChars = Math.max(
|
const totalMaxChars = resolveConfiguredBootstrapLimit(opts?.totalMaxChars);
|
||||||
1,
|
|
||||||
Math.floor(opts?.totalMaxChars ?? Math.max(maxChars, DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS)),
|
|
||||||
);
|
|
||||||
let remainingTotalChars = totalMaxChars;
|
let remainingTotalChars = totalMaxChars;
|
||||||
const result: EmbeddedContextFile[] = [];
|
const result: EmbeddedContextFile[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (remainingTotalChars <= 0) {
|
if (typeof remainingTotalChars === "number" && remainingTotalChars <= 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (file.missing) {
|
if (file.missing) {
|
||||||
const missingText = `[MISSING] Expected at: ${file.path}`;
|
const missingText = `[MISSING] Expected at: ${file.path}`;
|
||||||
const cappedMissingText = clampToBudget(missingText, remainingTotalChars);
|
const cappedMissingText =
|
||||||
|
typeof remainingTotalChars === "number"
|
||||||
|
? clampToBudget(missingText, remainingTotalChars)
|
||||||
|
: missingText;
|
||||||
if (!cappedMissingText) {
|
if (!cappedMissingText) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length);
|
if (typeof remainingTotalChars === "number") {
|
||||||
|
remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length);
|
||||||
|
}
|
||||||
result.push({
|
result.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
content: cappedMissingText,
|
content: cappedMissingText,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS) {
|
if (
|
||||||
|
typeof remainingTotalChars === "number" &&
|
||||||
|
remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS
|
||||||
|
) {
|
||||||
opts?.warn?.(
|
opts?.warn?.(
|
||||||
`remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`,
|
`remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const fileMaxChars = Math.max(1, Math.min(maxChars, remainingTotalChars));
|
const fileMaxChars = (() => {
|
||||||
|
if (typeof maxChars === "number" && typeof remainingTotalChars === "number") {
|
||||||
|
return Math.max(1, Math.min(maxChars, remainingTotalChars));
|
||||||
|
}
|
||||||
|
if (typeof maxChars === "number") {
|
||||||
|
return Math.max(1, maxChars);
|
||||||
|
}
|
||||||
|
if (typeof remainingTotalChars === "number") {
|
||||||
|
return Math.max(1, remainingTotalChars);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars);
|
const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars);
|
||||||
const contentWithinBudget = clampToBudget(trimmed.content, remainingTotalChars);
|
const contentWithinBudget =
|
||||||
|
typeof remainingTotalChars === "number"
|
||||||
|
? clampToBudget(trimmed.content, remainingTotalChars)
|
||||||
|
: trimmed.content;
|
||||||
if (!contentWithinBudget) {
|
if (!contentWithinBudget) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) {
|
if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) {
|
||||||
|
const limitLabel =
|
||||||
|
typeof trimmed.maxChars === "number" ? trimmed.maxChars : "configured total budget";
|
||||||
opts?.warn?.(
|
opts?.warn?.(
|
||||||
`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`,
|
`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${limitLabel}); truncating in injected context`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length);
|
if (typeof remainingTotalChars === "number") {
|
||||||
|
remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length);
|
||||||
|
}
|
||||||
result.push({
|
result.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
content: contentWithinBudget,
|
content: contentWithinBudget,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { createOllamaStreamFn, OLLAMA_NATIVE_BASE_URL } from "../../ollama-strea
|
|||||||
import {
|
import {
|
||||||
isCloudCodeAssistFormatError,
|
isCloudCodeAssistFormatError,
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
|
resolveBootstrapTotalMaxChars,
|
||||||
validateAnthropicTurns,
|
validateAnthropicTurns,
|
||||||
validateGeminiTurns,
|
validateGeminiTurns,
|
||||||
} from "../../pi-embedded-helpers.js";
|
} from "../../pi-embedded-helpers.js";
|
||||||
@@ -462,6 +463,7 @@ export async function runEmbeddedAttempt(
|
|||||||
model: params.modelId,
|
model: params.modelId,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
bootstrapMaxChars: resolveBootstrapMaxChars(params.config),
|
bootstrapMaxChars: resolveBootstrapMaxChars(params.config),
|
||||||
|
bootstrapTotalMaxChars: resolveBootstrapTotalMaxChars(params.config),
|
||||||
sandbox: (() => {
|
sandbox: (() => {
|
||||||
const runtime = resolveSandboxRuntimeStatus({
|
const runtime = resolveSandboxRuntimeStatus({
|
||||||
cfg: params.config,
|
cfg: params.config,
|
||||||
|
|||||||
@@ -44,4 +44,40 @@ describe("buildSystemPromptReport", () => {
|
|||||||
|
|
||||||
expect(report.injectedWorkspaceFiles[0]?.injectedChars).toBe("trimmed".length);
|
expect(report.injectedWorkspaceFiles[0]?.injectedChars).toBe("trimmed".length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks workspace files truncated when injected chars are smaller than raw chars", () => {
|
||||||
|
const file = makeBootstrapFile({
|
||||||
|
path: "/tmp/workspace/policies/AGENTS.md",
|
||||||
|
content: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
});
|
||||||
|
const report = buildSystemPromptReport({
|
||||||
|
source: "run",
|
||||||
|
generatedAt: 0,
|
||||||
|
systemPrompt: "system",
|
||||||
|
bootstrapFiles: [file],
|
||||||
|
injectedFiles: [{ path: "/tmp/workspace/policies/AGENTS.md", content: "trimmed" }],
|
||||||
|
skillsPrompt: "",
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(report.injectedWorkspaceFiles[0]?.truncated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes both bootstrap caps in the report payload", () => {
|
||||||
|
const file = makeBootstrapFile({ path: "/tmp/workspace/policies/AGENTS.md" });
|
||||||
|
const report = buildSystemPromptReport({
|
||||||
|
source: "run",
|
||||||
|
generatedAt: 0,
|
||||||
|
bootstrapMaxChars: 11_111,
|
||||||
|
bootstrapTotalMaxChars: 22_222,
|
||||||
|
systemPrompt: "system",
|
||||||
|
bootstrapFiles: [file],
|
||||||
|
injectedFiles: [{ path: "AGENTS.md", content: "trimmed" }],
|
||||||
|
skillsPrompt: "",
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(report.bootstrapMaxChars).toBe(11_111);
|
||||||
|
expect(report.bootstrapTotalMaxChars).toBe(22_222);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChar
|
|||||||
function buildInjectedWorkspaceFiles(params: {
|
function buildInjectedWorkspaceFiles(params: {
|
||||||
bootstrapFiles: WorkspaceBootstrapFile[];
|
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||||
injectedFiles: EmbeddedContextFile[];
|
injectedFiles: EmbeddedContextFile[];
|
||||||
bootstrapMaxChars: number;
|
|
||||||
}): SessionSystemPromptReport["injectedWorkspaceFiles"] {
|
}): SessionSystemPromptReport["injectedWorkspaceFiles"] {
|
||||||
const injectedByPath = new Map(params.injectedFiles.map((f) => [f.path, f.content]));
|
const injectedByPath = new Map(params.injectedFiles.map((f) => [f.path, f.content]));
|
||||||
const injectedByBaseName = new Map<string, string>();
|
const injectedByBaseName = new Map<string, string>();
|
||||||
@@ -57,7 +56,7 @@ function buildInjectedWorkspaceFiles(params: {
|
|||||||
injectedByPath.get(file.name) ??
|
injectedByPath.get(file.name) ??
|
||||||
injectedByBaseName.get(file.name);
|
injectedByBaseName.get(file.name);
|
||||||
const injectedChars = injected ? injected.length : 0;
|
const injectedChars = injected ? injected.length : 0;
|
||||||
const truncated = !file.missing && rawChars > params.bootstrapMaxChars;
|
const truncated = !file.missing && injectedChars < rawChars;
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: file.path,
|
path: file.path,
|
||||||
@@ -118,7 +117,8 @@ export function buildSystemPromptReport(params: {
|
|||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
bootstrapMaxChars: number;
|
bootstrapMaxChars?: number;
|
||||||
|
bootstrapTotalMaxChars?: number;
|
||||||
sandbox?: SessionSystemPromptReport["sandbox"];
|
sandbox?: SessionSystemPromptReport["sandbox"];
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
bootstrapFiles: WorkspaceBootstrapFile[];
|
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||||
@@ -148,6 +148,7 @@ export function buildSystemPromptReport(params: {
|
|||||||
model: params.model,
|
model: params.model,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
bootstrapMaxChars: params.bootstrapMaxChars,
|
bootstrapMaxChars: params.bootstrapMaxChars,
|
||||||
|
bootstrapTotalMaxChars: params.bootstrapTotalMaxChars,
|
||||||
sandbox: params.sandbox,
|
sandbox: params.sandbox,
|
||||||
systemPrompt: {
|
systemPrompt: {
|
||||||
chars: systemPrompt.length,
|
chars: systemPrompt.length,
|
||||||
@@ -157,7 +158,6 @@ export function buildSystemPromptReport(params: {
|
|||||||
injectedWorkspaceFiles: buildInjectedWorkspaceFiles({
|
injectedWorkspaceFiles: buildInjectedWorkspaceFiles({
|
||||||
bootstrapFiles: params.bootstrapFiles,
|
bootstrapFiles: params.bootstrapFiles,
|
||||||
injectedFiles: params.injectedFiles,
|
injectedFiles: params.injectedFiles,
|
||||||
bootstrapMaxChars: params.bootstrapMaxChars,
|
|
||||||
}),
|
}),
|
||||||
skills: {
|
skills: {
|
||||||
promptChars: params.skillsPrompt.length,
|
promptChars: params.skillsPrompt.length,
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import type { HandleCommandsParams } from "./commands-types.js";
|
|||||||
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
|
||||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||||
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
||||||
import { resolveBootstrapMaxChars } from "../../agents/pi-embedded-helpers.js";
|
import {
|
||||||
|
resolveBootstrapMaxChars,
|
||||||
|
resolveBootstrapTotalMaxChars,
|
||||||
|
} from "../../agents/pi-embedded-helpers.js";
|
||||||
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||||
@@ -59,6 +62,7 @@ async function resolveContextReport(
|
|||||||
|
|
||||||
const workspaceDir = params.workspaceDir;
|
const workspaceDir = params.workspaceDir;
|
||||||
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);
|
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);
|
||||||
|
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.cfg);
|
||||||
const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({
|
const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
@@ -169,6 +173,7 @@ async function resolveContextReport(
|
|||||||
model: params.model,
|
model: params.model,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
bootstrapMaxChars,
|
bootstrapMaxChars,
|
||||||
|
bootstrapTotalMaxChars,
|
||||||
sandbox: { mode: sandboxRuntime.mode, sandboxed: sandboxRuntime.sandboxed },
|
sandbox: { mode: sandboxRuntime.mode, sandboxed: sandboxRuntime.sandboxed },
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
bootstrapFiles,
|
bootstrapFiles,
|
||||||
@@ -249,7 +254,11 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
|||||||
const bootstrapMaxLabel =
|
const bootstrapMaxLabel =
|
||||||
typeof report.bootstrapMaxChars === "number"
|
typeof report.bootstrapMaxChars === "number"
|
||||||
? `${formatInt(report.bootstrapMaxChars)} chars`
|
? `${formatInt(report.bootstrapMaxChars)} chars`
|
||||||
: "? chars";
|
: "unlimited (unset)";
|
||||||
|
const bootstrapTotalLabel =
|
||||||
|
typeof report.bootstrapTotalMaxChars === "number"
|
||||||
|
? `${formatInt(report.bootstrapTotalMaxChars)} chars`
|
||||||
|
: "unlimited (unset)";
|
||||||
|
|
||||||
const totalsLine =
|
const totalsLine =
|
||||||
session.totalTokens != null
|
session.totalTokens != null
|
||||||
@@ -280,6 +289,7 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
|||||||
"🧠 Context breakdown (detailed)",
|
"🧠 Context breakdown (detailed)",
|
||||||
`Workspace: ${workspaceLabel}`,
|
`Workspace: ${workspaceLabel}`,
|
||||||
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
||||||
|
`Bootstrap max/total: ${bootstrapTotalLabel}`,
|
||||||
sandboxLine,
|
sandboxLine,
|
||||||
systemPromptLine,
|
systemPromptLine,
|
||||||
"",
|
"",
|
||||||
@@ -317,6 +327,7 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
|||||||
"🧠 Context breakdown",
|
"🧠 Context breakdown",
|
||||||
`Workspace: ${workspaceLabel}`,
|
`Workspace: ${workspaceLabel}`,
|
||||||
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
||||||
|
`Bootstrap max/total: ${bootstrapTotalLabel}`,
|
||||||
sandboxLine,
|
sandboxLine,
|
||||||
systemPromptLine,
|
systemPromptLine,
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).",
|
"auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).",
|
||||||
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
|
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
|
||||||
"agents.defaults.bootstrapMaxChars":
|
"agents.defaults.bootstrapMaxChars":
|
||||||
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
|
"Optional max characters of each workspace bootstrap file injected into the system prompt before truncation.",
|
||||||
"agents.defaults.bootstrapTotalMaxChars":
|
"agents.defaults.bootstrapTotalMaxChars":
|
||||||
"Max total characters across all injected workspace bootstrap files (default: 24000).",
|
"Optional max total characters across all injected workspace bootstrap files.",
|
||||||
"agents.defaults.repoRoot":
|
"agents.defaults.repoRoot":
|
||||||
"Optional repository root shown in the system prompt runtime line (overrides auto-detect).",
|
"Optional repository root shown in the system prompt runtime line (overrides auto-detect).",
|
||||||
"agents.defaults.envelopeTimezone":
|
"agents.defaults.envelopeTimezone":
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export type SessionSystemPromptReport = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
bootstrapMaxChars?: number;
|
bootstrapMaxChars?: number;
|
||||||
|
bootstrapTotalMaxChars?: number;
|
||||||
sandbox?: {
|
sandbox?: {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
sandboxed?: boolean;
|
sandboxed?: boolean;
|
||||||
|
|||||||
@@ -134,9 +134,9 @@ export type AgentDefaultsConfig = {
|
|||||||
repoRoot?: string;
|
repoRoot?: string;
|
||||||
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
||||||
skipBootstrap?: boolean;
|
skipBootstrap?: boolean;
|
||||||
/** Max chars for injected bootstrap files before truncation (default: 20000). */
|
/** Optional max chars for each injected bootstrap file before truncation. */
|
||||||
bootstrapMaxChars?: number;
|
bootstrapMaxChars?: number;
|
||||||
/** Max total chars across all injected bootstrap files (default: 24000). */
|
/** Optional max total chars across all injected bootstrap files. */
|
||||||
bootstrapTotalMaxChars?: number;
|
bootstrapTotalMaxChars?: number;
|
||||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||||
userTimezone?: string;
|
userTimezone?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user