diff --git a/CHANGELOG.md b/CHANGELOG.md index 244db768b1c..0af3f8ac908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai - Providers/DeepSeek: add DeepSeek V4 Flash and V4 Pro to the bundled catalog and make V4 Flash the onboarding default. Thanks @lsdsjy. - CLI/Gateway: make `gateway status` start faster by skipping plugin loading on the read-only status path. (#71364) Thanks @andyylin. - Plugins/compatibility: add a central plugin compatibility registry and docs for SDK/config/setup/runtime deprecation records, including dated migration metadata for legacy harness naming and other plugin-facing aliases. Thanks @vincentkoc. +- Agents/bootstrap: add `agents.defaults.contextInjection: "never"` to disable workspace bootstrap file injection for agents that fully own their prompt lifecycle. (#65006) Thanks @xDarkicex. ### Fixes diff --git a/src/agents/pi-embedded-runner/compact.hooks.harness.ts b/src/agents/pi-embedded-runner/compact.hooks.harness.ts index 986784225ab..9b63537f17a 100644 --- a/src/agents/pi-embedded-runner/compact.hooks.harness.ts +++ b/src/agents/pi-embedded-runner/compact.hooks.harness.ts @@ -339,6 +339,7 @@ export async function loadCompactHooksHarness(): Promise<{ vi.doMock("../bootstrap-files.js", () => ({ makeBootstrapWarn: vi.fn(() => () => {}), + resolveContextInjectionMode: vi.fn(() => "always"), resolveBootstrapContextForRun: vi.fn(async () => ({ contextFiles: [] })), })); diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 703a72a7dfe..169b6e1e204 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -37,7 +37,11 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; import { resolveOpenClawAgentDir } from "../agent-paths.js"; import { resolveSessionAgentIds } from "../agent-scope.js"; -import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js"; +import { + makeBootstrapWarn, + resolveBootstrapContextForRun, + resolveContextInjectionMode, +} from "../bootstrap-files.js"; import { listChannelSupportedActions, resolveChannelMessageToolCapabilities, @@ -471,17 +475,19 @@ export async function compactEmbeddedPiSessionDirect( const sessionLabel = params.sessionKey ?? params.sessionId; const resolvedMessageProvider = params.messageChannel ?? params.messageProvider; - const { contextFiles } = await resolveBootstrapContextForRun({ - workspaceDir: effectiveWorkspace, - config: params.config, - sessionKey: params.sessionKey, - sessionId: params.sessionId, - warn: makeBootstrapWarn({ - sessionLabel, - workspaceDir: effectiveWorkspace, - warn: (message) => log.warn(message), - }), - }); + const contextInjectionMode = resolveContextInjectionMode(params.config); + const { contextFiles } = contextInjectionMode === "never" + ? { contextFiles: [] } + : await resolveBootstrapContextForRun({ + workspaceDir: effectiveWorkspace, + config: params.config, + sessionKey: params.sessionKey, + sessionId: params.sessionId, + warn: makeBootstrapWarn({ + sessionLabel, + warn: (message) => log.warn(message), + }), + }); // Apply contextTokens cap to model so pi-coding-agent's auto-compaction // threshold uses the effective limit, not the native context window. const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel; diff --git a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts b/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts index c8270fef280..362bc6d1094 100644 --- a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts +++ b/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts @@ -13,23 +13,23 @@ export { export type AttemptContextEngine = ContextEngine; -export type AttemptBootstrapContext = { - bootstrapFiles: unknown[]; - contextFiles: unknown[]; +export type AttemptBootstrapContext = { + bootstrapFiles: TBootstrapFile[]; + contextFiles: TContextFile[]; }; -export async function resolveAttemptBootstrapContext< - TContext extends AttemptBootstrapContext, ->(params: { - contextInjectionMode: "always" | "continuation-skip"; +export async function resolveAttemptBootstrapContext(params: { + contextInjectionMode: "always" | "continuation-skip" | "never"; bootstrapContextMode?: string; bootstrapContextRunKind?: string; bootstrapMode?: BootstrapMode; sessionFile: string; hasCompletedBootstrapTurn: (sessionFile: string) => Promise; - resolveBootstrapContextForRun: () => Promise; + resolveBootstrapContextForRun: () => Promise< + AttemptBootstrapContext + >; }): Promise< - TContext & { + AttemptBootstrapContext & { isContinuationTurn: boolean; shouldRecordCompletedBootstrapTurn: boolean; } @@ -39,14 +39,16 @@ export async function resolveAttemptBootstrapContext< params.contextInjectionMode === "continuation-skip" && params.bootstrapContextRunKind !== "heartbeat" && (await params.hasCompletedBootstrapTurn(params.sessionFile)); + const shouldSkipBootstrapInjection = + params.contextInjectionMode === "never" || isContinuationTurn; const shouldRecordCompletedBootstrapTurn = - !isContinuationTurn && + !shouldSkipBootstrapInjection && params.bootstrapContextMode !== "lightweight" && params.bootstrapContextRunKind !== "heartbeat" && params.bootstrapMode === "full"; - const context = isContinuationTurn - ? ({ bootstrapFiles: [], contextFiles: [] } as unknown as TContext) + const context = shouldSkipBootstrapInjection + ? { bootstrapFiles: [], contextFiles: [] } : await params.resolveBootstrapContextForRun(); return { diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts index b58112e25ba..751c9e033d5 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts @@ -12,7 +12,7 @@ import { import { resetEmbeddedAttemptHarness } from "./attempt.spawn-workspace.test-support.js"; async function resolveBootstrapContext(params: { - contextInjectionMode?: "always" | "continuation-skip"; + contextInjectionMode?: "always" | "continuation-skip" | "never"; bootstrapContextMode?: string; bootstrapContextRunKind?: string; bootstrapMode?: "full" | "limited" | "none"; @@ -77,6 +77,22 @@ describe("embedded attempt context injection", () => { expect(resolver).toHaveBeenCalledTimes(1); }); + it("disables bootstrap injection without marking the turn as a continuation", async () => { + const { result, hasCompletedBootstrapTurn, resolveBootstrapContextForRun } = + await resolveBootstrapContext({ + contextInjectionMode: "never", + bootstrapMode: "full", + completed: true, + }); + + expect(result.isContinuationTurn).toBe(false); + expect(result.shouldRecordCompletedBootstrapTurn).toBe(false); + expect(result.bootstrapFiles).toEqual([]); + expect(result.contextFiles).toEqual([]); + expect(hasCompletedBootstrapTurn).not.toHaveBeenCalled(); + expect(resolveBootstrapContextForRun).not.toHaveBeenCalled(); + }); + it("does not let a stale completed marker suppress pending workspace bootstrap", async () => { const resolver = vi.fn(async () => ({ bootstrapFiles: [{ name: "BOOTSTRAP.md" }], diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 9649d1f9ff2..5e66ea99c2c 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -3447,6 +3447,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "string", const: "continuation-skip", }, + { + type: "string", + const: "never", + }, ], title: "Context Injection", description: diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 96337d50c9f..aaa9cfd8542 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -15,7 +15,7 @@ import type { } from "./types.base.js"; import type { MemorySearchConfig } from "./types.tools.js"; -export type AgentContextInjection = "always" | "continuation-skip"; +export type AgentContextInjection = "always" | "continuation-skip" | "never"; export type EmbeddedPiExecutionContract = "default" | "strict-agentic"; export type Gpt5PromptOverlayConfig = { diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 069c495cb4b..7e4d2dc3dab 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -52,8 +52,13 @@ describe("agent defaults schema", () => { expect(result.contextInjection).toBe("continuation-skip"); }); + it("accepts contextInjection: never", () => { + const result = AgentDefaultsSchema.parse({ contextInjection: "never" })!; + expect(result.contextInjection).toBe("never"); + }); + it("rejects invalid contextInjection values", () => { - expect(() => AgentDefaultsSchema.parse({ contextInjection: "never" })).toThrow(); + expect(() => AgentDefaultsSchema.parse({ contextInjection: "unknown" })).toThrow(); }); it("accepts embeddedPi.executionContract", () => { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 6eb243e991b..faaeea571b3 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -83,7 +83,9 @@ export const AgentDefaultsSchema = z .strict() .optional(), skipBootstrap: z.boolean().optional(), - contextInjection: z.union([z.literal("always"), z.literal("continuation-skip")]).optional(), + contextInjection: z + .union([z.literal("always"), z.literal("continuation-skip"), z.literal("never")]) + .optional(), bootstrapMaxChars: z.number().int().positive().optional(), bootstrapTotalMaxChars: z.number().int().positive().optional(), experimental: z