mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
feat(workspace): add skipOptionalBootstrapFiles config option (#62110)
Adds `agents.defaults.skipOptionalBootstrapFiles` for optional workspace bootstrap files, validates the supported filenames, and propagates the option through workspace bootstrap callers. Also preserves legacy setup detection when `USER.md` or `IDENTITY.md` are intentionally skipped, documents the config field, and includes focused regression coverage. Landing follow-up included small CI unblockers for current-base drift: removing an unused Brave runtime dependency, fixing Telegram RTT lint, and preserving compatible gateway-bindable plugin registry cache reuse when runtime ensures disable bundled dependency installation.
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/workspace: add `agents.defaults.skipOptionalBootstrapFiles` for skipping selected optional workspace files during bootstrap without disabling required workspace setup. (#62110) Thanks @mainstay22.
|
||||
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
|
||||
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
|
||||
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
d70e31fd5f36d4b117ffa750fba88072d6714edc245a18d4b0915a2d11ce603a config-baseline.json
|
||||
0a259216178a582c567d1fa48c5236bff4bbd27c3e6af838ffcd042459ffce3c config-baseline.core.json
|
||||
516de8f5049d2c8b7f326cfc1b665cf459609aa491c432d93b8ca8b9463d7243 config-baseline.json
|
||||
b06e5cd6e7d3a26d99fd4d31d576c49958195451b0b1e9c2db45f038a3c16c44 config-baseline.core.json
|
||||
da8e055ebba0730498703d209f9e2cfaa1484a83f3240e611dcdd7280e22a525 config-baseline.channel.json
|
||||
4d017161b4dc986fdc6cc68167fedbd1d415ddbcd66125a872e18aa1769cd182 config-baseline.plugin.json
|
||||
|
||||
@@ -67,6 +67,20 @@ Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md`
|
||||
}
|
||||
```
|
||||
|
||||
### `agents.defaults.skipOptionalBootstrapFiles`
|
||||
|
||||
Skips creation of selected optional workspace files while still writing required bootstrap files. Valid values: `SOUL.md`, `USER.md`, `HEARTBEAT.md`, and `IDENTITY.md`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
skipOptionalBootstrapFiles: ["SOUL.md", "USER.md"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### `agents.defaults.contextInjection`
|
||||
|
||||
Controls when workspace bootstrap files are injected into the system prompt. Default: `"always"`.
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"typebox": "1.1.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
|
||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -327,10 +327,6 @@ importers:
|
||||
version: link:../../packages/plugin-sdk
|
||||
|
||||
extensions/brave:
|
||||
dependencies:
|
||||
typebox:
|
||||
specifier: 1.1.34
|
||||
version: 1.1.34
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
|
||||
@@ -10,12 +10,12 @@ const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ??
|
||||
const canaryTimeoutMs = Number(
|
||||
process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs),
|
||||
);
|
||||
const scenarioIds = (
|
||||
process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply"
|
||||
)
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const scenarioIds = new Set(
|
||||
(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
if (!groupId || !driverToken || !sutToken) {
|
||||
throw new Error(
|
||||
@@ -63,10 +63,6 @@ function messageText(message) {
|
||||
return message.text ?? message.caption ?? "";
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function flushUpdates(bot) {
|
||||
let updates = await bot.getUpdates({ timeout: 0, allowed_updates: ["message"] });
|
||||
let nextOffset;
|
||||
@@ -194,7 +190,7 @@ async function main() {
|
||||
}),
|
||||
);
|
||||
|
||||
if (scenarioIds.includes("telegram-mentioned-message-reply")) {
|
||||
if (scenarioIds.has("telegram-mentioned-message-reply")) {
|
||||
const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`;
|
||||
scenarios.push(
|
||||
await runScenario({
|
||||
|
||||
@@ -373,6 +373,7 @@ async function prepareAgentCommandExecution(
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
const runId = opts.runId?.trim() || sessionId;
|
||||
|
||||
@@ -49,6 +49,7 @@ async function ensureSandboxWorkspaceLayout(params: {
|
||||
sandboxWorkspaceDir,
|
||||
agentWorkspaceDir,
|
||||
params.config?.agents?.defaults?.skipBootstrap,
|
||||
params.config?.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
);
|
||||
if (cfg.workspaceAccess !== "rw") {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import syncFs from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OptionalBootstrapFileName } from "../../config/types.agent-defaults.js";
|
||||
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ export async function ensureSandboxWorkspace(
|
||||
workspaceDir: string,
|
||||
seedFrom?: string,
|
||||
skipBootstrap?: boolean,
|
||||
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[],
|
||||
) {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
if (seedFrom) {
|
||||
@@ -61,5 +63,6 @@ export async function ensureSandboxWorkspace(
|
||||
await ensureAgentWorkspace({
|
||||
dir: workspaceDir,
|
||||
ensureBootstrapFiles: !skipBootstrap,
|
||||
skipOptionalBootstrapFiles,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,6 +155,55 @@ describe("ensureAgentWorkspace", () => {
|
||||
await expectCompletedWithoutBootstrap(tempDir);
|
||||
});
|
||||
|
||||
it("skips configured optional bootstrap files without skipping required files", async () => {
|
||||
const tempDir = await makeTempWorkspace("openclaw-workspace-");
|
||||
|
||||
await ensureAgentWorkspace({
|
||||
dir: tempDir,
|
||||
ensureBootstrapFiles: true,
|
||||
skipOptionalBootstrapFiles: [
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
DEFAULT_IDENTITY_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
],
|
||||
});
|
||||
|
||||
await expect(fs.access(path.join(tempDir, DEFAULT_AGENTS_FILENAME))).resolves.toBeUndefined();
|
||||
await expect(fs.access(path.join(tempDir, DEFAULT_TOOLS_FILENAME))).resolves.toBeUndefined();
|
||||
await expect(
|
||||
fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)),
|
||||
).resolves.toBeUndefined();
|
||||
for (const fileName of [
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
DEFAULT_IDENTITY_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
]) {
|
||||
await expect(fs.access(path.join(tempDir, fileName))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves legacy setup detection when skipped profile files already exist", async () => {
|
||||
const tempDir = await makeTempWorkspace("openclaw-workspace-");
|
||||
await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_IDENTITY_FILENAME, content: "custom" });
|
||||
await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_USER_FILENAME, content: "custom" });
|
||||
|
||||
await ensureAgentWorkspace({
|
||||
dir: tempDir,
|
||||
ensureBootstrapFiles: true,
|
||||
skipOptionalBootstrapFiles: [DEFAULT_IDENTITY_FILENAME, DEFAULT_USER_FILENAME],
|
||||
});
|
||||
|
||||
await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
const state = await readWorkspaceState(tempDir);
|
||||
expect(state.setupCompletedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
it("migrates legacy onboardingCompletedAt markers to setupCompletedAt", async () => {
|
||||
const tempDir = await makeTempWorkspace("openclaw-workspace-");
|
||||
await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true });
|
||||
|
||||
@@ -174,6 +174,13 @@ const VALID_BOOTSTRAP_NAMES: ReadonlySet<string> = new Set([
|
||||
DEFAULT_MEMORY_FILENAME,
|
||||
]);
|
||||
|
||||
const OPTIONAL_BOOTSTRAP_FILENAMES: ReadonlySet<string> = new Set([
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
DEFAULT_IDENTITY_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
]);
|
||||
|
||||
async function writeFileIfMissing(filePath: string, content: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.writeFile(filePath, content, {
|
||||
@@ -467,6 +474,12 @@ async function ensureGitRepo(dir: string, isBrandNewWorkspace: boolean) {
|
||||
export async function ensureAgentWorkspace(params?: {
|
||||
dir?: string;
|
||||
ensureBootstrapFiles?: boolean;
|
||||
/**
|
||||
* List of optional bootstrap filenames to skip writing.
|
||||
* Applies only to SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.
|
||||
* Required workspace setup such as AGENTS.md and TOOLS.md still runs.
|
||||
*/
|
||||
skipOptionalBootstrapFiles?: string[];
|
||||
}): Promise<{
|
||||
dir: string;
|
||||
agentsPath?: string;
|
||||
@@ -519,12 +532,24 @@ export async function ensureAgentWorkspace(params?: {
|
||||
const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME);
|
||||
const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME);
|
||||
const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME);
|
||||
const skipOptionalBootstrapFiles = new Set(params?.skipOptionalBootstrapFiles ?? []);
|
||||
const shouldWriteBootstrapFile = (fileName: string): boolean =>
|
||||
!OPTIONAL_BOOTSTRAP_FILENAMES.has(fileName) || !skipOptionalBootstrapFiles.has(fileName);
|
||||
|
||||
await writeFileIfMissing(agentsPath, agentsTemplate);
|
||||
await writeFileIfMissing(soulPath, soulTemplate);
|
||||
if (shouldWriteBootstrapFile(DEFAULT_SOUL_FILENAME)) {
|
||||
await writeFileIfMissing(soulPath, soulTemplate);
|
||||
}
|
||||
await writeFileIfMissing(toolsPath, toolsTemplate);
|
||||
const identityPathCreated = await writeFileIfMissing(identityPath, identityTemplate);
|
||||
await writeFileIfMissing(userPath, userTemplate);
|
||||
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
||||
const identityPathCreated = shouldWriteBootstrapFile(DEFAULT_IDENTITY_FILENAME)
|
||||
? await writeFileIfMissing(identityPath, identityTemplate)
|
||||
: false;
|
||||
if (shouldWriteBootstrapFile(DEFAULT_USER_FILENAME)) {
|
||||
await writeFileIfMissing(userPath, userTemplate);
|
||||
}
|
||||
if (shouldWriteBootstrapFile(DEFAULT_HEARTBEAT_FILENAME)) {
|
||||
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
||||
}
|
||||
|
||||
let state = await readWorkspaceSetupState(statePath, {
|
||||
persistLegacyMigration: true,
|
||||
|
||||
@@ -242,6 +242,7 @@ export async function getReplyFromConfig(
|
||||
: await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv,
|
||||
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
|
||||
@@ -177,6 +177,7 @@ export async function agentsAddCommand(
|
||||
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
|
||||
await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, {
|
||||
skipBootstrap: Boolean(bindingResult.config.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: bindingResult.config.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
agentId,
|
||||
});
|
||||
|
||||
@@ -431,6 +432,7 @@ export async function agentsAddCommand(
|
||||
logConfigUpdated(runtime);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
agentId,
|
||||
});
|
||||
|
||||
|
||||
@@ -589,7 +589,10 @@ export async function runConfigureWizard(
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
};
|
||||
|
||||
const configureChannelsSection = async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/wor
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import { resolveConfigPath } from "../config/paths.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveControlUiLinks } from "../gateway/control-ui-links.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
@@ -164,11 +165,16 @@ function resolveSshTargetHint(): string {
|
||||
export async function ensureWorkspaceAndSessions(
|
||||
workspaceDir: string,
|
||||
runtime: RuntimeEnv,
|
||||
options?: { skipBootstrap?: boolean; agentId?: string },
|
||||
options?: {
|
||||
skipBootstrap?: boolean;
|
||||
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
|
||||
agentId?: string;
|
||||
},
|
||||
) {
|
||||
const ws = await ensureAgentWorkspace({
|
||||
dir: workspaceDir,
|
||||
ensureBootstrapFiles: !options?.skipBootstrap,
|
||||
skipOptionalBootstrapFiles: options?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent(options?.agentId);
|
||||
|
||||
@@ -214,6 +214,7 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
|
||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
|
||||
@@ -84,6 +84,42 @@ describe("setupCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("threads skipOptionalBootstrapFiles into workspace creation", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
const configPath = path.join(configDir, "openclaw.json");
|
||||
const deps = createSetupDeps(home);
|
||||
const workspace = path.join(home, "custom-workspace");
|
||||
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace,
|
||||
skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await setupCommand(undefined, runtime, deps);
|
||||
|
||||
expect(deps.ensureAgentWorkspace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dir: workspace,
|
||||
skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("treats non-object config roots as empty config", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const runtime = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import JSON5 from "json5";
|
||||
import { z } from "zod";
|
||||
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -16,6 +17,7 @@ type ConfigIO = {
|
||||
type EnsureAgentWorkspace = (params: {
|
||||
dir: string;
|
||||
ensureBootstrapFiles?: boolean;
|
||||
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
|
||||
}) => Promise<{ dir: string }>;
|
||||
|
||||
type SetupCommandDeps = {
|
||||
@@ -191,6 +193,7 @@ export async function setupCommand(
|
||||
const ws = await (deps.ensureAgentWorkspace ?? ensureDefaultAgentWorkspace)({
|
||||
dir: workspace,
|
||||
ensureBootstrapFiles: !next.agents?.defaults?.skipBootstrap,
|
||||
skipOptionalBootstrapFiles: next.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);
|
||||
|
||||
|
||||
@@ -3690,6 +3690,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
skipBootstrap: {
|
||||
type: "boolean",
|
||||
},
|
||||
skipOptionalBootstrapFiles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"],
|
||||
},
|
||||
title: "Skipped Optional Bootstrap Files",
|
||||
description:
|
||||
"Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
|
||||
},
|
||||
contextInjection: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -26184,6 +26194,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: 'Friendly interaction-style layer for GPT-5-family models ("friendly" or "on" enables it; "off" disables only that layer). The tagged behavior contract remains enabled for matching GPT-5 models.',
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"agents.defaults.skipOptionalBootstrapFiles": {
|
||||
label: "Skipped Optional Bootstrap Files",
|
||||
help: "Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"agents.defaults.contextInjection": {
|
||||
label: "Context Injection",
|
||||
help: 'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.',
|
||||
|
||||
@@ -958,6 +958,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Maximum same-provider auth-profile rotations allowed for rate-limit errors before switching to model fallback (default: 1).",
|
||||
"agents.defaults.workspace":
|
||||
"Default workspace path exposed to agent runtime tools for filesystem context and repo-aware behavior. Set this explicitly when running from wrappers so path resolution stays deterministic.",
|
||||
"agents.defaults.skipOptionalBootstrapFiles":
|
||||
"Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.",
|
||||
"agents.defaults.contextInjection":
|
||||
'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.',
|
||||
"agents.defaults.bootstrapMaxChars":
|
||||
|
||||
@@ -387,6 +387,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.promptOverlays": "Prompt Overlays",
|
||||
"agents.defaults.promptOverlays.gpt5": "GPT-5 Prompt Overlay",
|
||||
"agents.defaults.promptOverlays.gpt5.personality": "GPT-5 Personality Overlay",
|
||||
"agents.defaults.skipOptionalBootstrapFiles": "Skipped Optional Bootstrap Files",
|
||||
"agents.defaults.contextInjection": "Context Injection",
|
||||
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
|
||||
"agents.defaults.bootstrapTotalMaxChars": "Bootstrap Total Max Chars",
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
import type { MemorySearchConfig } from "./types.tools.js";
|
||||
|
||||
export type AgentContextInjection = "always" | "continuation-skip" | "never";
|
||||
export type OptionalBootstrapFileName = "SOUL.md" | "USER.md" | "HEARTBEAT.md" | "IDENTITY.md";
|
||||
export type EmbeddedPiExecutionContract = "default" | "strict-agentic";
|
||||
|
||||
export type Gpt5PromptOverlayConfig = {
|
||||
@@ -224,6 +225,13 @@ export type AgentDefaultsConfig = {
|
||||
promptOverlays?: PromptOverlaysConfig;
|
||||
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */
|
||||
skipBootstrap?: boolean;
|
||||
/**
|
||||
* List of optional bootstrap filenames to skip writing to the workspace root.
|
||||
* Applies to: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.
|
||||
* Required workspace setup such as AGENTS.md and TOOLS.md still runs.
|
||||
* Example: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"]
|
||||
*/
|
||||
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
|
||||
/**
|
||||
* Controls when workspace bootstrap files (AGENTS.md, SOUL.md, etc.) are
|
||||
* injected into the system prompt:
|
||||
|
||||
@@ -83,6 +83,25 @@ describe("agent defaults schema", () => {
|
||||
expect(() => AgentDefaultsSchema.parse({ contextInjection: "unknown" })).toThrow();
|
||||
});
|
||||
|
||||
it("accepts supported optional bootstrap filenames", () => {
|
||||
const result = AgentDefaultsSchema.parse({
|
||||
skipOptionalBootstrapFiles: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"],
|
||||
})!;
|
||||
expect(result.skipOptionalBootstrapFiles).toEqual([
|
||||
"SOUL.md",
|
||||
"USER.md",
|
||||
"HEARTBEAT.md",
|
||||
"IDENTITY.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects unsupported optional bootstrap filenames", () => {
|
||||
expect(() =>
|
||||
AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["AGENTS.md"] }),
|
||||
).toThrow();
|
||||
expect(() => AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["SOUL.MD"] })).toThrow();
|
||||
});
|
||||
|
||||
it("accepts embeddedPi.executionContract", () => {
|
||||
const result = AgentDefaultsSchema.parse({
|
||||
embeddedPi: {
|
||||
|
||||
@@ -24,6 +24,13 @@ const NonNegativeByteSizeSchema = z.union([
|
||||
z.string().refine(isValidNonNegativeByteSizeString, "Expected byte size string like 2mb"),
|
||||
]);
|
||||
|
||||
const OptionalBootstrapFileNameSchema = z.enum([
|
||||
"SOUL.md",
|
||||
"USER.md",
|
||||
"HEARTBEAT.md",
|
||||
"IDENTITY.md",
|
||||
]);
|
||||
|
||||
export const SilentReplyPolicyConfigSchema = z
|
||||
.object({
|
||||
direct: SilentReplyPolicySchema.optional(),
|
||||
@@ -89,6 +96,7 @@ export const AgentDefaultsSchema = z
|
||||
.strict()
|
||||
.optional(),
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
skipOptionalBootstrapFiles: z.array(OptionalBootstrapFileNameSchema).optional(),
|
||||
contextInjection: z
|
||||
.union([z.literal("always"), z.literal("continuation-skip"), z.literal("never")])
|
||||
.optional(),
|
||||
@@ -231,9 +239,7 @@ export const AgentDefaultsSchema = z
|
||||
])
|
||||
.optional(),
|
||||
verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(),
|
||||
reasoningDefault: z
|
||||
.union([z.literal("off"), z.literal("on"), z.literal("stream")])
|
||||
.optional(),
|
||||
reasoningDefault: z.union([z.literal("off"), z.literal("on"), z.literal("stream")]).optional(),
|
||||
elevatedDefault: z
|
||||
.union([z.literal("off"), z.literal("on"), z.literal("ask"), z.literal("full")])
|
||||
.optional(),
|
||||
|
||||
@@ -519,6 +519,7 @@ async function prepareCronRunContext(params: {
|
||||
const workspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDirRaw,
|
||||
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !params.isFastTestEnv,
|
||||
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
const workspaceDir = workspace.dir;
|
||||
|
||||
|
||||
@@ -494,7 +494,11 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||
// Ensure workspace & transcripts exist BEFORE writing config so a failure
|
||||
// here does not leave a broken config entry behind.
|
||||
const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
|
||||
await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
|
||||
await ensureAgentWorkspace({
|
||||
dir: workspaceDir,
|
||||
ensureBootstrapFiles: !skipBootstrap,
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true });
|
||||
|
||||
const persistedIdentity = normalizeIdentityForFile(resolveAgentIdentity(nextConfig, agentId));
|
||||
@@ -575,6 +579,7 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||
ensuredWorkspace = await ensureAgentWorkspace({
|
||||
dir: workspaceDir,
|
||||
ensureBootstrapFiles: !skipBootstrap,
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -687,6 +687,24 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function pluginLoadOptionsMatchCacheKey(
|
||||
options: PluginLoadOptions,
|
||||
expectedCacheKey: string,
|
||||
): boolean {
|
||||
if (resolvePluginLoadCacheContext(options).cacheKey === expectedCacheKey) {
|
||||
return true;
|
||||
}
|
||||
if (options.installBundledRuntimeDeps !== false) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
resolvePluginLoadCacheContext({
|
||||
...options,
|
||||
installBundledRuntimeDeps: undefined,
|
||||
}).cacheKey === expectedCacheKey
|
||||
);
|
||||
}
|
||||
|
||||
type PluginRegistrationPlan = {
|
||||
/** Public compatibility label passed to plugin register(api). */
|
||||
mode: PluginRegistrationMode;
|
||||
@@ -896,15 +914,15 @@ function getCompatibleActivePluginRegistry(
|
||||
return undefined;
|
||||
}
|
||||
const loadContext = resolvePluginLoadCacheContext(options);
|
||||
if (loadContext.cacheKey === activeCacheKey) {
|
||||
if (pluginLoadOptionsMatchCacheKey(options, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingCacheKey = resolvePluginLoadCacheContext({
|
||||
const activatingOptions = {
|
||||
...options,
|
||||
activate: true,
|
||||
}).cacheKey;
|
||||
if (activatingCacheKey === activeCacheKey) {
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
@@ -912,26 +930,26 @@ function getCompatibleActivePluginRegistry(
|
||||
loadContext.runtimeSubagentMode === "default" &&
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
|
||||
) {
|
||||
const gatewayBindableCacheKey = resolvePluginLoadCacheContext({
|
||||
const gatewayBindableOptions = {
|
||||
...options,
|
||||
runtimeOptions: {
|
||||
...options.runtimeOptions,
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}).cacheKey;
|
||||
if (gatewayBindableCacheKey === activeCacheKey) {
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(gatewayBindableOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingGatewayBindableCacheKey = resolvePluginLoadCacheContext({
|
||||
const activatingGatewayBindableOptions = {
|
||||
...options,
|
||||
activate: true,
|
||||
runtimeOptions: {
|
||||
...options.runtimeOptions,
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}).cacheKey;
|
||||
if (activatingGatewayBindableCacheKey === activeCacheKey) {
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingGatewayBindableOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,6 +741,7 @@ export async function runSetupWizard(
|
||||
logConfigUpdated(runtime);
|
||||
await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
|
||||
if (opts.skipSearch) {
|
||||
|
||||
Reference in New Issue
Block a user