refactor(agents): remove stale runner state plumbing (#118495)

* refactor(agents): remove stale runner state plumbing

* refactor(agents): keep bootstrap warning owner reachable

* refactor(agents): inline bootstrap warning predicates

* refactor(agents): extract bootstrap warning contracts

* refactor(agents): keep bootstrap detail type private

* refactor(auto-reply): drop vacuous abort returns
This commit is contained in:
Peter Steinberger
2026-08-03 01:59:31 -07:00
committed by GitHub
parent f62e42b69f
commit e797e69969
24 changed files with 383 additions and 488 deletions
+22
View File
@@ -0,0 +1,22 @@
import { copyPluginToolMeta } from "../plugins/tools.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { copyBeforeToolCallHookMarker } from "./before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "./channel-tool-metadata.js";
import { copyCodeModeControlToolIdentity } from "./code-mode-control-tools.js";
import { copyToolTerminalPresentation } from "./tool-terminal-presentation.js";
/**
* Preserve identity-backed tool metadata that object spread cannot carry.
* Losing it detaches policy, hooks, presentation, and control-flow ownership.
*/
export function copyAgentToolMetadata<T extends AnyAgentTool>(source: AnyAgentTool, target: T): T {
if (source === target) {
return target;
}
copyPluginToolMeta(source, target);
copyChannelAgentToolMeta(source as never, target as never);
copyBeforeToolCallHookMarker(source, target);
copyToolTerminalPresentation(source, target);
copyCodeModeControlToolIdentity(source, target);
return target;
}
+4 -9
View File
@@ -1,13 +1,11 @@
import { createAbortError } from "../infra/abort-signal.js";
/**
* Abort-signal wrapping for agent tools.
* Combines per-call cancellation with run-level aborts while preserving plugin,
* channel, and before_tool_call metadata on wrapped tools.
* Combines per-call cancellation with run-level aborts while preserving
* identity-backed metadata on wrapped tools.
*/
import { copyPluginToolMeta } from "../plugins/tools.js";
import { copyAgentToolMetadata } from "./agent-tool-metadata.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { copyBeforeToolCallHookMarker } from "./before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "./channel-tools.js";
function throwAbortError(): never {
throw createAbortError("Aborted");
@@ -72,8 +70,5 @@ export function wrapToolWithAbortSignal(
);
},
};
copyPluginToolMeta(tool, wrappedTool);
copyChannelAgentToolMeta(tool as never, wrappedTool as never);
copyBeforeToolCallHookMarker(tool, wrappedTool);
return wrappedTool;
return copyAgentToolMetadata(tool, wrappedTool);
}
@@ -18,7 +18,6 @@ export {
peekAdjustedParamsForToolCall,
} from "./agent-tools.before-tool-call.state.js";
export {
copyBeforeToolCallHookMarker,
isToolWrappedWithBeforeToolCallHook,
setBeforeToolCallDiagnosticsEnabled,
} from "./before-tool-call-metadata.js";
+2 -9
View File
@@ -1,4 +1,4 @@
import { copyPluginToolMeta } from "../plugins/tools.js";
import { copyAgentToolMetadata } from "./agent-tool-metadata.js";
/**
* Adjusts exec/process tool descriptions for long-running follow-up behavior.
* Cron-aware runs can point models at scheduled follow-ups; cronless runs keep
@@ -6,18 +6,11 @@ import { copyPluginToolMeta } from "../plugins/tools.js";
*/
import type { AnyAgentTool } from "./agent-tools.types.js";
import { describeExecTool, describeProcessTool } from "./bash-tools.descriptions.js";
import { copyBeforeToolCallHookMarker } from "./before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "./channel-tools.js";
import { copyToolTerminalPresentation } from "./tool-terminal-presentation.js";
import { isAutomationsToolName } from "./tools/automations-tool-name.js";
function replaceDescription(tool: AnyAgentTool, description: string): AnyAgentTool {
const updated = { ...tool, description };
copyPluginToolMeta(tool, updated);
copyChannelAgentToolMeta(tool as never, updated as never);
copyBeforeToolCallHookMarker(tool, updated);
copyToolTerminalPresentation(tool, updated);
return updated;
return copyAgentToolMetadata(tool, updated);
}
/** Return tools with exec/process descriptions adjusted for cron availability. */
+16
View File
@@ -10,6 +10,10 @@ import {
} from "./agent-tools.ring-zero-context.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { stubTool } from "./test-helpers/fast-tool-stubs.js";
import {
getToolTerminalPresentation,
setToolTerminalPresentation,
} from "./tool-terminal-presentation.js";
type ExecuteMock = ReturnType<typeof vi.fn>;
@@ -215,6 +219,18 @@ describe("wrapToolWithAbortSignal", () => {
});
expect(execute).not.toHaveBeenCalled();
});
it("preserves terminal presentation metadata on abort-wrapped tools", () => {
const formatter = () => ({ text: "done" });
const tool = setToolTerminalPresentation(
asAgentTool({ name: "presented", execute: vi.fn() }),
formatter,
);
const wrapped = wrapToolWithAbortSignal(tool, new AbortController().signal);
expect(getToolTerminalPresentation(wrapped)).toBe(formatter);
});
});
vi.mock("./channel-tools.js", () => {
+4 -14
View File
@@ -4,14 +4,11 @@ import {
} from "@openclaw/ai/internal/openai";
/**
* Tool schema normalization wrappers.
* Applies provider-compatible parameter schema cleanup while preserving plugin
* and channel metadata on normalized tools.
* Applies provider-compatible parameter schema cleanup while preserving
* identity-backed metadata on normalized tools.
*/
import { copyPluginToolMeta } from "../plugins/tools.js";
import { copyAgentToolMetadata } from "./agent-tool-metadata.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { copyBeforeToolCallHookMarker } from "./before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "./channel-tools.js";
import { copyToolTerminalPresentation } from "./tool-terminal-presentation.js";
function isObjectSchemaWithNoRequiredParams(schema: unknown): boolean {
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
@@ -69,13 +66,6 @@ export function normalizeToolParameters(
tool: AnyAgentTool,
options?: ToolParameterSchemaOptions,
): AnyAgentTool {
function preserveToolMeta(target: AnyAgentTool): AnyAgentTool {
copyPluginToolMeta(tool, target);
copyChannelAgentToolMeta(tool as never, target as never);
copyBeforeToolCallHookMarker(tool, target);
copyToolTerminalPresentation(tool, target);
return target;
}
const schema =
tool.parameters && typeof tool.parameters === "object"
? (tool.parameters as Record<string, unknown>)
@@ -84,7 +74,7 @@ export function normalizeToolParameters(
return tool;
}
const parameters = normalizeToolParameterSchema(schema, options);
return preserveToolMeta({
return copyAgentToolMetadata(tool, {
...tool,
...addEmptyObjectArgumentPreparation(tool, parameters),
parameters,
+173
View File
@@ -0,0 +1,173 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type {
BootstrapBudgetAnalysis,
BootstrapPromptWarning,
BootstrapPromptWarningMode,
BootstrapTruncationCause,
} from "./bootstrap-budget.types.js";
import { USER_BOOTSTRAP_MAX_CHARS } from "./embedded-agent-helpers/bootstrap.js";
const DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
const DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
function formatWarningCause(cause: BootstrapTruncationCause): string {
return cause === "per-file-limit" ? "max/file" : "max/total";
}
export function normalizeBootstrapWarningSignatures(signatures?: string[]): string[] {
if (!Array.isArray(signatures) || signatures.length === 0) {
return [];
}
const seen = new Set<string>();
const result: string[] = [];
for (const signature of signatures) {
const value = normalizeOptionalString(signature) ?? "";
if (!value || seen.has(value)) {
continue;
}
seen.add(value);
result.push(value);
}
return result;
}
function appendSeenSignature(signatures: string[], signature: string): string[] {
if (!signature.trim() || signatures.includes(signature)) {
return signatures;
}
const next = [...signatures, signature];
return next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX
? next
: next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
}
function buildBootstrapTruncationSignature(analysis: BootstrapBudgetAnalysis): string | undefined {
if (!analysis.hasTruncation) {
return undefined;
}
const files = analysis.truncatedFiles
.map((file) => ({
path: file.path || file.name,
rawChars: file.rawChars,
injectedChars: file.injectedChars,
causes: [...file.causes].toSorted(),
}))
.toSorted((a, b) => {
const pathCmp = a.path.localeCompare(b.path);
if (pathCmp !== 0) {
return pathCmp;
}
if (a.rawChars !== b.rawChars) {
return a.rawChars - b.rawChars;
}
if (a.injectedChars !== b.injectedChars) {
return a.injectedChars - b.injectedChars;
}
return a.causes.join("+").localeCompare(b.causes.join("+"));
});
return JSON.stringify({
bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
files,
});
}
function formatBootstrapTruncationWarningLines(params: {
analysis: BootstrapBudgetAnalysis;
maxFiles?: number;
}): string[] {
if (!params.analysis.hasTruncation) {
return [];
}
const maxFiles =
typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0
? Math.floor(params.maxFiles)
: DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
const lines: string[] = [];
const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
return acc;
}, new Map<string, number>());
const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
for (const file of topFiles) {
const pct =
file.rawChars > 0
? Math.round(((file.rawChars - file.injectedChars) / file.rawChars) * 100)
: 0;
const causeText =
file.causes.length > 0
? file.causes.map((cause) => formatWarningCause(cause)).join(", ")
: "";
const nameLabel =
(duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0
? `${file.name} (${file.path})`
: file.name;
lines.push(
`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`,
);
}
if (params.analysis.truncatedFiles.length > topFiles.length) {
lines.push(
`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`,
);
}
if (params.analysis.truncatedFiles.some((file) => file.name?.toLowerCase() === "agents.md")) {
lines.push("AGENTS.md was truncated; read the full AGENTS.md before relying on scoped policy.");
}
const fixedUserCapApplied = params.analysis.truncatedFiles.some(
(file) =>
file.name?.toLowerCase() === "user.md" &&
file.effectiveFileLimit === USER_BOOTSTRAP_MAX_CHARS &&
file.causes.includes("per-file-limit"),
);
if (fixedUserCapApplied) {
lines.push(
`USER.md has a fixed ${USER_BOOTSTRAP_MAX_CHARS}-character bootstrap cap; keep it compact.`,
);
}
const configurableLimitApplied = params.analysis.truncatedFiles.some(
(file) =>
file.name?.toLowerCase() !== "user.md" ||
file.effectiveFileLimit < USER_BOOTSTRAP_MAX_CHARS ||
file.causes.includes("total-limit"),
);
if (configurableLimitApplied) {
lines.push(
"If unintentional, raise agents.defaults.bootstrapMaxChars and/or agents.defaults.bootstrapTotalMaxChars.",
);
}
return lines;
}
/** Decides whether to show a prompt warning and returns the updated dedupe state. */
export function buildBootstrapPromptWarning(params: {
analysis: BootstrapBudgetAnalysis;
mode: BootstrapPromptWarningMode;
previousSignature?: string;
seenSignatures?: string[];
maxFiles?: number;
}): BootstrapPromptWarning {
const signature = buildBootstrapTruncationSignature(params.analysis);
let seenSignatures = normalizeBootstrapWarningSignatures(params.seenSignatures);
if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
}
const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
const warningShown =
params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
const warningSignaturesSeen =
signature && params.mode !== "off"
? appendSeenSignature(seenSignatures, signature)
: seenSignatures;
return {
signature,
warningShown,
lines: warningShown
? formatBootstrapTruncationWarningLines({
analysis: params.analysis,
maxFiles: params.maxFiles,
})
: [],
warningSignaturesSeen,
};
}
+38 -1
View File
@@ -1,10 +1,11 @@
/** Tests bootstrap context truncation accounting and user-facing warning metadata. */
import { describe, expect, it } from "vitest";
import { buildBootstrapPromptWarning } from "./bootstrap-budget-warning.js";
import {
appendBootstrapPromptWarning,
analyzeBootstrapBudget,
buildBootstrapBudgetState,
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
buildBootstrapPromptWarningNotice,
buildBootstrapTruncationReportMeta,
resolveBootstrapWarningSignaturesSeen,
@@ -12,6 +13,42 @@ import {
import { buildAgentSystemPrompt } from "./system-prompt.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
describe("buildBootstrapBudgetState", () => {
it("composes configured limits, ordered injection stats, and warning state", () => {
const bootstrapFiles: WorkspaceBootstrapFile[] = [
{
name: "AGENTS.md",
path: "/tmp/AGENTS.md",
content: "a".repeat(8),
missing: false,
},
{
name: "SOUL.md",
path: "/tmp/SOUL.md",
content: "b".repeat(8),
missing: false,
},
];
const state = buildBootstrapBudgetState({
config: {
agents: { defaults: { bootstrapMaxChars: 10, bootstrapTotalMaxChars: 12 } },
},
bootstrapFiles,
injectedFiles: [
{ path: "/tmp/AGENTS.md", content: "a".repeat(8) },
{ path: "/tmp/SOUL.md", content: "b".repeat(4) },
],
});
expect(state.bootstrapMaxChars).toBe(10);
expect(state.bootstrapTotalMaxChars).toBe(12);
expect(state.bootstrapPromptWarningMode).toBe("always");
expect(state.bootstrapAnalysis.truncatedFiles[0]?.causes).toEqual(["total-limit"]);
expect(state.bootstrapPromptWarning.warningShown).toBe(true);
});
});
describe("buildBootstrapInjectionStats", () => {
it("maps raw and injected sizes and marks truncation", () => {
const bootstrapFiles: WorkspaceBootstrapFile[] = [
+47 -215
View File
@@ -4,54 +4,27 @@
*/
import path from "node:path";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
buildBootstrapPromptWarning,
normalizeBootstrapWarningSignatures,
} from "./bootstrap-budget-warning.js";
import type {
BootstrapBudgetAnalysis,
BootstrapInjectionStat,
BootstrapPromptWarning,
BootstrapPromptWarningMode,
BootstrapTruncationCause,
} from "./bootstrap-budget.types.js";
import type { EmbeddedContextFile } from "./embedded-agent-helpers.js";
import { USER_BOOTSTRAP_MAX_CHARS } from "./embedded-agent-helpers/bootstrap.js";
import {
resolveBootstrapMaxChars,
resolveBootstrapTotalMaxChars,
USER_BOOTSTRAP_MAX_CHARS,
} from "./embedded-agent-helpers/bootstrap.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
const DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
const DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
const DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
type BootstrapTruncationCause = "per-file-limit" | "total-limit";
type BootstrapPromptWarningMode = "off" | "once" | "always";
type BootstrapInjectionStat = {
name: string;
path: string;
missing: boolean;
rawChars: number;
injectedChars: number;
truncated: boolean;
};
type BootstrapAnalyzedFile = BootstrapInjectionStat & {
effectiveFileLimit: number;
nearLimit: boolean;
causes: BootstrapTruncationCause[];
};
type BootstrapBudgetAnalysis = {
files: BootstrapAnalyzedFile[];
truncatedFiles: BootstrapAnalyzedFile[];
nearLimitFiles: BootstrapAnalyzedFile[];
totalNearLimit: boolean;
hasTruncation: boolean;
totals: {
rawChars: number;
injectedChars: number;
truncatedChars: number;
bootstrapMaxChars: number;
bootstrapTotalMaxChars: number;
nearLimitRatio: number;
};
};
type BootstrapPromptWarning = {
signature?: string;
warningShown: boolean;
lines: string[];
warningSignaturesSeen: string[];
};
type BootstrapTruncationReportMeta = {
warningMode: BootstrapPromptWarningMode;
@@ -70,55 +43,12 @@ function normalizePositiveLimit(value: number): number {
return Math.floor(value);
}
function formatWarningCause(cause: BootstrapTruncationCause): string {
return cause === "per-file-limit" ? "max/file" : "max/total";
}
function isAgentsBootstrapName(name: string | undefined): boolean {
return name?.toLowerCase() === "agents.md";
}
function isUserBootstrapName(name: string | undefined): boolean {
return name?.toLowerCase() === "user.md";
}
function effectiveBootstrapFileLimit(name: string, bootstrapMaxChars: number): number {
return name.toLowerCase() === "user.md"
? Math.min(bootstrapMaxChars, USER_BOOTSTRAP_MAX_CHARS)
: bootstrapMaxChars;
}
function normalizeSeenSignatures(signatures?: string[]): string[] {
if (!Array.isArray(signatures) || signatures.length === 0) {
return [];
}
const seen = new Set<string>();
const result: string[] = [];
for (const signature of signatures) {
const value = normalizeOptionalString(signature) ?? "";
if (!value || seen.has(value)) {
continue;
}
seen.add(value);
result.push(value);
}
return result;
}
function appendSeenSignature(signatures: string[], signature: string): string[] {
if (!signature.trim()) {
return signatures;
}
if (signatures.includes(signature)) {
return signatures;
}
const next = [...signatures, signature];
if (next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX) {
return next;
}
return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
}
/** Restores prompt-warning dedupe state from a previous bootstrap report. */
export function resolveBootstrapWarningSignaturesSeen(report?: {
bootstrapTruncation?: {
@@ -128,7 +58,7 @@ export function resolveBootstrapWarningSignaturesSeen(report?: {
};
}): string[] {
const truncation = report?.bootstrapTruncation;
const seenFromReport = normalizeSeenSignatures(truncation?.warningSignaturesSeen);
const seenFromReport = normalizeBootstrapWarningSignatures(truncation?.warningSignaturesSeen);
if (seenFromReport.length > 0) {
return seenFromReport;
}
@@ -253,136 +183,38 @@ export function analyzeBootstrapBudget(params: {
};
}
/** Builds a stable signature for once-per-truncation warning suppression. */
function buildBootstrapTruncationSignature(analysis: BootstrapBudgetAnalysis): string | undefined {
if (!analysis.hasTruncation) {
return undefined;
}
const files = analysis.truncatedFiles
.map((file) => ({
path: file.path || file.name,
rawChars: file.rawChars,
injectedChars: file.injectedChars,
causes: [...file.causes].toSorted(),
}))
.toSorted((a, b) => {
const pathCmp = a.path.localeCompare(b.path);
if (pathCmp !== 0) {
return pathCmp;
}
if (a.rawChars !== b.rawChars) {
return a.rawChars - b.rawChars;
}
if (a.injectedChars !== b.injectedChars) {
return a.injectedChars - b.injectedChars;
}
return a.causes.join("+").localeCompare(b.causes.join("+"));
});
return JSON.stringify({
bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
files,
});
}
/** Formats human-readable warning lines for the most important truncated files. */
function formatBootstrapTruncationWarningLines(params: {
analysis: BootstrapBudgetAnalysis;
maxFiles?: number;
}): string[] {
if (!params.analysis.hasTruncation) {
return [];
}
const maxFiles =
typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0
? Math.floor(params.maxFiles)
: DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
const lines: string[] = [];
const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
return acc;
}, new Map<string, number>());
const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
for (const file of topFiles) {
const pct =
file.rawChars > 0
? Math.round(((file.rawChars - file.injectedChars) / file.rawChars) * 100)
: 0;
const causeText =
file.causes.length > 0
? file.causes.map((cause) => formatWarningCause(cause)).join(", ")
: "";
const nameLabel =
(duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0
? `${file.name} (${file.path})`
: file.name;
lines.push(
`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`,
);
}
if (params.analysis.truncatedFiles.length > topFiles.length) {
lines.push(
`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`,
);
}
if (params.analysis.truncatedFiles.some((file) => isAgentsBootstrapName(file.name))) {
lines.push("AGENTS.md was truncated; read the full AGENTS.md before relying on scoped policy.");
}
const fixedUserCapApplied = params.analysis.truncatedFiles.some(
(file) =>
isUserBootstrapName(file.name) &&
file.effectiveFileLimit === USER_BOOTSTRAP_MAX_CHARS &&
file.causes.includes("per-file-limit"),
);
if (fixedUserCapApplied) {
lines.push(
`USER.md has a fixed ${USER_BOOTSTRAP_MAX_CHARS}-character bootstrap cap; keep it compact.`,
);
}
const configurableLimitApplied = params.analysis.truncatedFiles.some(
(file) =>
!isUserBootstrapName(file.name) ||
file.effectiveFileLimit < USER_BOOTSTRAP_MAX_CHARS ||
file.causes.includes("total-limit"),
);
if (configurableLimitApplied) {
lines.push(
"If unintentional, raise agents.defaults.bootstrapMaxChars and/or agents.defaults.bootstrapTotalMaxChars.",
);
}
return lines;
}
/** Decides whether to show a prompt warning and returns the updated dedupe state. */
export function buildBootstrapPromptWarning(params: {
analysis: BootstrapBudgetAnalysis;
mode: BootstrapPromptWarningMode;
/** Builds the canonical bootstrap budget diagnosis after caller-owned routing. */
export function buildBootstrapBudgetState(params: {
config?: OpenClawConfig;
agentId?: string | null;
bootstrapFiles: WorkspaceBootstrapFile[];
injectedFiles: EmbeddedContextFile[];
previousSignature?: string;
seenSignatures?: string[];
maxFiles?: number;
}): BootstrapPromptWarning {
const signature = buildBootstrapTruncationSignature(params.analysis);
let seenSignatures = normalizeSeenSignatures(params.seenSignatures);
if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
}
const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
const warningShown =
params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
const warningSignaturesSeen =
signature && params.mode !== "off"
? appendSeenSignature(seenSignatures, signature)
: seenSignatures;
}) {
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config, params.agentId);
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config, params.agentId);
const bootstrapAnalysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles: params.bootstrapFiles,
injectedFiles: params.injectedFiles,
}),
bootstrapMaxChars,
bootstrapTotalMaxChars,
});
const bootstrapPromptWarningMode: BootstrapPromptWarningMode = "always";
const bootstrapPromptWarning = buildBootstrapPromptWarning({
analysis: bootstrapAnalysis,
mode: bootstrapPromptWarningMode,
seenSignatures: params.seenSignatures,
previousSignature: params.previousSignature,
});
return {
signature,
warningShown,
lines: warningShown
? formatBootstrapTruncationWarningLines({
analysis: params.analysis,
maxFiles: params.maxFiles,
})
: [],
warningSignaturesSeen,
bootstrapAnalysis,
bootstrapMaxChars,
bootstrapPromptWarning,
bootstrapPromptWarningMode,
bootstrapTotalMaxChars,
};
}
+40
View File
@@ -0,0 +1,40 @@
export type BootstrapTruncationCause = "per-file-limit" | "total-limit";
export type BootstrapPromptWarningMode = "off" | "once" | "always";
export type BootstrapInjectionStat = {
name: string;
path: string;
missing: boolean;
rawChars: number;
injectedChars: number;
truncated: boolean;
};
type BootstrapAnalyzedFile = BootstrapInjectionStat & {
effectiveFileLimit: number;
nearLimit: boolean;
causes: BootstrapTruncationCause[];
};
export type BootstrapBudgetAnalysis = {
files: BootstrapAnalyzedFile[];
truncatedFiles: BootstrapAnalyzedFile[];
nearLimitFiles: BootstrapAnalyzedFile[];
totalNearLimit: boolean;
hasTruncation: boolean;
totals: {
rawChars: number;
injectedChars: number;
truncatedChars: number;
bootstrapMaxChars: number;
bootstrapTotalMaxChars: number;
nearLimitRatio: number;
};
};
export type BootstrapPromptWarning = {
signature?: string;
warningShown: boolean;
lines: string[];
warningSignaturesSeen: string[];
};
+10 -20
View File
@@ -61,10 +61,8 @@ import { resolveAuthProfileOrder } from "../auth-profiles/order.js";
import { loadAuthProfileStoreForRuntime } from "../auth-profiles/store.js";
import type { AuthProfileCredential, AuthProfileStore } from "../auth-profiles/types.js";
import {
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
buildBootstrapBudgetState,
buildBootstrapTruncationReportMeta,
analyzeBootstrapBudget,
} from "../bootstrap-budget.js";
import {
makeBootstrapWarn as makeBootstrapWarnImpl,
@@ -85,11 +83,6 @@ import {
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { resolveContextTokensForModel } from "../context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import {
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "../embedded-agent-helpers.js";
import {
applyEmbeddedAttemptToolsAllow,
mergeForcedEmbeddedAttemptToolsAllow,
@@ -881,20 +874,17 @@ export async function prepareCliRunContext(
const bootstrapFilesForInjectionStats = includeBootstrapInSystemContext
? bootstrapFiles
: bootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME);
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config, sessionAgentId);
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config, sessionAgentId);
const bootstrapAnalysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles: bootstrapFilesForInjectionStats,
injectedFiles: contextFiles,
}),
const {
bootstrapAnalysis,
bootstrapMaxChars,
bootstrapPromptWarning,
bootstrapPromptWarningMode,
bootstrapTotalMaxChars,
});
const bootstrapPromptWarningMode = resolveBootstrapPromptTruncationWarningMode(params.config);
const bootstrapPromptWarning = buildBootstrapPromptWarning({
analysis: bootstrapAnalysis,
mode: bootstrapPromptWarningMode,
} = buildBootstrapBudgetState({
config: params.config,
agentId: sessionAgentId,
bootstrapFiles: bootstrapFilesForInjectionStats,
injectedFiles: contextFiles,
seenSignatures: params.bootstrapPromptWarningSignaturesSeen,
previousSignature: params.bootstrapPromptWarningSignature,
});
@@ -4,7 +4,6 @@ import type { OpenClawConfig } from "../config/config.js";
import {
buildBootstrapContextFiles,
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "./embedded-agent-helpers.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
@@ -12,7 +11,6 @@ import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
const EXPECTED_DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000;
const EXPECTED_DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 60_000;
const EXPECTED_DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE = "always";
const makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
name: DEFAULT_AGENTS_FILENAME,
@@ -369,36 +367,3 @@ describe("bootstrap limit resolvers", () => {
}
});
});
describe("resolveBootstrapPromptTruncationWarningMode", () => {
it("defaults to always", () => {
expect(resolveBootstrapPromptTruncationWarningMode()).toBe("always");
expect(EXPECTED_DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE).toBe("always");
});
it("ignores retired explicit modes", () => {
expect(
resolveBootstrapPromptTruncationWarningMode({
agents: { defaults: { bootstrapPromptTruncationWarning: "off" } },
} as OpenClawConfig),
).toBe("always");
expect(
resolveBootstrapPromptTruncationWarningMode({
agents: { defaults: { bootstrapPromptTruncationWarning: "once" } },
} as OpenClawConfig),
).toBe("always");
expect(
resolveBootstrapPromptTruncationWarningMode({
agents: { defaults: { bootstrapPromptTruncationWarning: "always" } },
} as OpenClawConfig),
).toBe("always");
});
it("falls back to default for invalid values", () => {
expect(
resolveBootstrapPromptTruncationWarningMode({
agents: { defaults: { bootstrapPromptTruncationWarning: "invalid" } },
} as unknown as OpenClawConfig),
).toBe(EXPECTED_DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE);
});
});
-1
View File
@@ -3,7 +3,6 @@
export {
buildBootstrapContextFiles,
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "./embedded-agent-helpers/bootstrap.js";
export {
@@ -91,7 +91,6 @@ const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 60_000;
// USER.md stays directive-sized so profile guidance cannot crowd out project
// rules or durable facts from the shared bootstrap budget.
export const USER_BOOTSTRAP_MAX_CHARS = 4_000;
const DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE = "always";
const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64;
// Ratios split `contentBudget` (= maxChars marker.length join separators), not `maxChars`.
// The marker and "\n" separators are already reserved before this split runs; these ratios
@@ -146,12 +145,6 @@ export function resolveBootstrapTotalMaxChars(
return DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS;
}
export function resolveBootstrapPromptTruncationWarningMode(
_cfg?: OpenClawConfig,
): "off" | "once" | "always" {
return DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE;
}
function isAgentsBootstrapFile(fileName: string | undefined): boolean {
return fileName?.toLowerCase() === AGENTS_BOOTSTRAP_FILENAME.toLowerCase();
}
@@ -1,9 +1,5 @@
import { isEmbeddedMode } from "../../../infra/embedded-mode.js";
import {
analyzeBootstrapBudget,
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
} from "../../bootstrap-budget.js";
import { buildBootstrapBudgetState } from "../../bootstrap-budget.js";
import {
buildBootstrapContextForFiles,
hasCompletedBootstrapTurn,
@@ -16,11 +12,6 @@ import {
isPrimaryBootstrapRun,
resolveWorkspaceBootstrapRouting,
} from "../../bootstrap-routing.js";
import {
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "../../embedded-agent-helpers.js";
import {
DEFAULT_BOOTSTRAP_FILENAME,
isWorkspaceBootstrapPending,
@@ -144,23 +135,11 @@ export async function prepareEmbeddedAttemptBootstrap(params: {
const bootstrapFilesForInjectionStats = bootstrapRouting.includeBootstrapInSystemContext
? hookAdjustedBootstrapFiles
: hookAdjustedBootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME);
const bootstrapMaxChars = resolveBootstrapMaxChars(attempt.config, params.sessionAgentId);
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(
attempt.config,
params.sessionAgentId,
);
const bootstrapAnalysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles: bootstrapFilesForInjectionStats,
injectedFiles: contextFiles,
}),
bootstrapMaxChars,
bootstrapTotalMaxChars,
});
const bootstrapPromptWarningMode = resolveBootstrapPromptTruncationWarningMode(attempt.config);
const bootstrapPromptWarning = buildBootstrapPromptWarning({
analysis: bootstrapAnalysis,
mode: bootstrapPromptWarningMode,
const bootstrapBudget = buildBootstrapBudgetState({
config: attempt.config,
agentId: params.sessionAgentId,
bootstrapFiles: bootstrapFilesForInjectionStats,
injectedFiles: contextFiles,
seenSignatures: attempt.bootstrapPromptWarningSignaturesSeen,
previousSignature: attempt.bootstrapPromptWarningSignature,
});
@@ -179,12 +158,8 @@ export async function prepareEmbeddedAttemptBootstrap(params: {
}
return {
bootstrapAnalysis,
bootstrapMaxChars,
...bootstrapBudget,
bootstrapMode,
bootstrapPromptWarning,
bootstrapPromptWarningMode,
bootstrapTotalMaxChars,
contextFiles,
hookAdjustedBootstrapFiles,
shouldRecordCompletedBootstrapTurn,
@@ -1,10 +1,10 @@
// Coverage for bootstrap warning text in system prompt assembly.
import { describe, expect, it } from "vitest";
import { buildBootstrapPromptWarning } from "../../bootstrap-budget-warning.js";
import {
analyzeBootstrapBudget,
buildBootstrapPromptWarningNotice,
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
} from "../../bootstrap-budget.js";
import { composeSystemPromptWithHookContext } from "./attempt.thread-helpers.js";
@@ -1,5 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getPluginToolMeta, setPluginToolMeta } from "../../../plugins/tools.js";
import {
BEFORE_TOOL_CALL_SOURCE_TOOL,
BEFORE_TOOL_CALL_WRAPPED,
isToolWrappedWithBeforeToolCallHook,
} from "../../before-tool-call-metadata.js";
import { getChannelAgentToolMeta, setChannelAgentToolMeta } from "../../channel-tool-metadata.js";
import { isCodeModeControlTool, markCodeModeControlTool } from "../../code-mode-control-tools.js";
import {
@@ -127,21 +132,20 @@ describe("heartbeat wrapper metadata preservation", () => {
it("preserves before-tool-call marker on heartbeat-wrapped tools", () => {
const source: Record<string, unknown> = { name: "test-tool", execute: vi.fn() as never };
// Simulate a tool that has gone through the before-tool-call hook
Object.defineProperty(source, Symbol.for("openclaw:beforeToolCallWrapped"), {
Object.defineProperty(source, BEFORE_TOOL_CALL_WRAPPED, {
value: true,
enumerable: true,
});
Object.defineProperty(source, Symbol.for("openclaw:beforeToolCallSourceTool"), {
value: { name: "inner-tool" },
const sourceTool = { name: "inner-tool" };
Object.defineProperty(source, BEFORE_TOOL_CALL_SOURCE_TOOL, {
value: sourceTool,
enumerable: false,
});
const wrapped = wrapEmbeddedAttemptToolWithActivity(source as never, RUN) as typeof source;
expect((wrapped as Record<symbol, unknown>)[Symbol.for("openclaw:beforeToolCallWrapped")]).toBe(
true,
);
expect(isToolWrappedWithBeforeToolCallHook(wrapped as never)).toBe(true);
expect((wrapped as Record<symbol, unknown>)[BEFORE_TOOL_CALL_SOURCE_TOOL]).toBe(sourceTool);
});
it("preserves terminal presentation metadata on heartbeat-wrapped tools", () => {
@@ -1,14 +1,10 @@
import { copyPluginToolMeta } from "../../../plugins/tools.js";
import {
clearToolActivityRun,
getLastToolActivityMs,
notifyToolActivity,
onToolActivity,
} from "../../../shared/tool-activity-heartbeat.js";
import { copyBeforeToolCallHookMarker } from "../../agent-tools.before-tool-call.js";
import { copyChannelAgentToolMeta } from "../../channel-tools.js";
import { copyCodeModeControlToolIdentity } from "../../code-mode-control-tools.js";
import { copyToolTerminalPresentation } from "../../tool-terminal-presentation.js";
import { copyAgentToolMetadata } from "../../agent-tool-metadata.js";
import type { AnyAgentTool } from "../../tools/common.js";
export { clearToolActivityRun, getLastToolActivityMs, notifyToolActivity, onToolActivity };
@@ -33,11 +29,6 @@ export function wrapEmbeddedAttemptToolWithActivity<T extends AnyAgentTool>(
}
}) as typeof originalExecute,
} as T;
// Tool metadata lives in identity-keyed WeakMaps, so object spread is insufficient.
copyPluginToolMeta(tool, wrappedTool);
copyChannelAgentToolMeta(tool, wrappedTool);
copyBeforeToolCallHookMarker(tool, wrappedTool);
copyToolTerminalPresentation(tool, wrappedTool);
copyCodeModeControlToolIdentity(tool, wrappedTool);
return wrappedTool;
// Tool metadata is identity-keyed, so object spread is insufficient.
return copyAgentToolMetadata(tool, wrappedTool);
}
+2 -8
View File
@@ -7,9 +7,7 @@ import type { TSchema } from "typebox";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ProviderRuntimePluginHandle } from "../../plugins/provider-hook-runtime.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { copyPluginToolMeta } from "../../plugins/tools.js";
import { copyBeforeToolCallHookMarker } from "../before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "../channel-tools.js";
import { copyAgentToolMetadata } from "../agent-tool-metadata.js";
import {
logProviderToolSchemaDiagnostics,
normalizeProviderToolSchemas,
@@ -19,7 +17,6 @@ import {
filterProviderNormalizableTools,
type RuntimeToolSchemaDiagnostic,
} from "../tool-schema-projection.js";
import { copyToolTerminalPresentation } from "../tool-terminal-presentation.js";
import type { AnyAgentTool } from "../tools/common.js";
import type { AgentRuntimePlan } from "./types.js";
@@ -71,10 +68,7 @@ function copyRuntimeToolMetadata(source: AgentTool, target: AgentTool): void {
if (source.outputSchema !== undefined) {
target.outputSchema = source.outputSchema;
}
copyPluginToolMeta(source as never, target as never);
copyChannelAgentToolMeta(source as never, target as never);
copyBeforeToolCallHookMarker(source as never, target as never);
copyToolTerminalPresentation(source as never, target as never);
copyAgentToolMetadata(source as never, target as never);
}
// Duplicate names cannot be matched by map lookup alone, so same-index matches
+2 -9
View File
@@ -1,9 +1,6 @@
// Ambient trusted caller context for model-mediated Gateway tool calls.
import { AsyncLocalStorage } from "node:async_hooks";
import { copyPluginToolMeta } from "../../plugins/tools.js";
import { copyBeforeToolCallHookMarker } from "../before-tool-call-metadata.js";
import { copyChannelAgentToolMeta } from "../channel-tools.js";
import { copyToolTerminalPresentation } from "../tool-terminal-presentation.js";
import { copyAgentToolMetadata } from "../agent-tool-metadata.js";
import type { AnyAgentTool } from "./common.js";
type GatewayToolCallerIdentity = {
@@ -76,11 +73,7 @@ export function wrapToolWithGatewayCallerIdentity(
execute: async (...args) =>
await withGatewayToolCallerIdentity(identity, async () => await tool.execute?.(...args)),
};
copyPluginToolMeta(tool, wrapped);
copyChannelAgentToolMeta(tool as never, wrapped as never);
copyBeforeToolCallHookMarker(tool, wrapped);
copyToolTerminalPresentation(tool, wrapped);
return wrapped;
return copyAgentToolMetadata(tool, wrapped);
}
export function createGatewayToolCallerWrapper(
@@ -75,7 +75,6 @@ export async function chooseDispatchRoute(state: PrepareDispatchOperationReadySt
const shouldSuppressDefaultToolProgressMessages = () => !shouldEmitVerboseProgress();
const shouldSendVerboseProgressMessages = () => !shouldSuppressDefaultToolProgressMessages();
const shouldSendToolSummaries = () => shouldSendVerboseProgressMessages();
const shouldSendToolStartStatuses = false;
const notifiedSessionMetadataChangeKeys = new Set<string>();
const routeState: { sessionMetadataChangesForResult?: CommandSessionMetadataChange[] } = {};
const notifySessionMetadataChanges = (
@@ -600,7 +599,6 @@ export async function chooseDispatchRoute(state: PrepareDispatchOperationReadySt
shouldSuppressDefaultToolProgressMessages,
shouldSendVerboseProgressMessages,
shouldSendToolSummaries,
shouldSendToolStartStatuses,
notifySessionMetadataChanges,
shouldDeliverVerboseProgressDespiteSourceSuppression,
shouldDeliverForcedToolProgressDespiteSourceSuppression,
@@ -41,7 +41,6 @@ export async function executeDispatch(state: PrepareDispatchExecutionReadyState)
markProgress,
markVisibleToolErrorProgress,
maybeApplyTtsWithFinalizationLease,
maybeSendWorkingStatus,
normalizeReplyMediaPayload,
notifySessionMetadataChanges,
onToolResultFromReplyOptions,
@@ -339,24 +338,6 @@ export async function executeDispatch(state: PrepareDispatchExecutionReadyState)
) {
await state.onApprovalEventFromReplyOptions?.(payload);
}
if (isDispatchOperationAborted()) {
return;
}
if (
payload.phase !== "requested" ||
shouldSuppressDefaultToolProgressMessages()
) {
return;
}
const label = state.summarizeApprovalLabel({
status: payload.status,
command: payload.command,
message: payload.message,
});
if (!label) {
return;
}
await maybeSendWorkingStatus(label);
},
onPatchSummary: async (payload) => {
if (isDispatchOperationAborted()) {
@@ -378,20 +359,6 @@ export async function executeDispatch(state: PrepareDispatchExecutionReadyState)
) {
await state.onPatchSummaryFromReplyOptions?.(payload);
}
if (isDispatchOperationAborted()) {
return;
}
if (payload.phase !== "end" || shouldSuppressDefaultToolProgressMessages()) {
return;
}
const label = state.summarizePatchLabel({
summary: payload.summary,
title: payload.title,
});
if (!label) {
return;
}
await maybeSendWorkingStatus(label);
},
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
markProgress();
@@ -1,5 +1,4 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import {
isFastModeAutoProgressPayload,
resolveSendableOutboundReplyParts,
@@ -55,16 +54,7 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt
);
}
const toolStartStatusesSent = new Set<string>();
let toolStartStatusCount = 0;
let didSendPlanStatusNotice = false;
const normalizeWorkingLabel = (label: string) => {
const collapsed = label.replace(/\s+/g, " ").trim();
if (collapsed.length <= 80) {
return collapsed;
}
return `${truncateUtf16Safe(collapsed, 77).trimEnd()}...`;
};
const formatPlanUpdateText = (payload: { explanation?: string; steps?: AgentPlanStep[] }) => {
const explanation = payload.explanation?.replace(/\s+/g, " ").trim();
const steps = (payload.steps ?? [])
@@ -78,32 +68,6 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt
}
return explanation || "Planning next steps.";
};
const maybeSendWorkingStatus = async (label: string): Promise<void> => {
if (shouldSuppressProgressDelivery()) {
return;
}
const normalizedLabel = normalizeWorkingLabel(label);
if (
!shouldEmitVerboseProgress() ||
!state.shouldSendToolStartStatuses ||
!normalizedLabel ||
toolStartStatusCount >= 2 ||
toolStartStatusesSent.has(normalizedLabel)
) {
return;
}
toolStartStatusesSent.add(normalizedLabel);
toolStartStatusCount += 1;
const payload: ReplyPayload = {
text: `Working: ${normalizedLabel}`,
};
if (shouldRouteToOriginating) {
await sendPayloadAsync(payload, undefined, false);
return;
}
markInboundDedupeReplayUnsafe();
turnLedger.sendQueued("tool", payload);
};
const sendPlanUpdate = async (payload: {
explanation?: string;
steps?: AgentPlanStep[];
@@ -127,38 +91,6 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt
markInboundDedupeReplayUnsafe();
turnLedger.sendQueued("tool", replyPayload);
};
const summarizeApprovalLabel = (payload: {
status?: string;
command?: string;
message?: string;
}) => {
if (payload.status === "pending") {
const command = normalizeOptionalString(payload.command);
if (command) {
return normalizeWorkingLabel(`awaiting approval: ${command}`);
}
return "awaiting approval";
}
if (payload.status === "unavailable") {
const message = normalizeOptionalString(payload.message);
if (message) {
return normalizeWorkingLabel(message);
}
return "approval unavailable";
}
return "";
};
const summarizePatchLabel = (payload: { summary?: string; title?: string }) => {
const summary = normalizeOptionalString(payload.summary);
if (summary) {
return normalizeWorkingLabel(summary);
}
const title = normalizeOptionalString(payload.title);
if (title) {
return normalizeWorkingLabel(title);
}
return "";
};
// Track accumulated block text for TTS generation after streaming completes.
// When block streaming succeeds, there's no final reply, so we need to generate
// TTS audio separately from the accumulated block content.
@@ -484,10 +416,7 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt
: withFullRuntimeReplyConfig(cfg);
state.recordAgentDispatchStarted();
const nextState = extendPreparedDispatchState(state, {
maybeSendWorkingStatus,
sendPlanUpdate,
summarizeApprovalLabel,
summarizePatchLabel,
cleanBlockTtsDirectiveText,
resolveToolDeliveryPayload,
typing,
@@ -1,11 +1,11 @@
// Prompt composition scenarios build reusable agent prompt fixtures.
import fs from "node:fs/promises";
import path from "node:path";
import { buildBootstrapPromptWarning } from "../../../src/agents/bootstrap-budget-warning.js";
import {
appendBootstrapPromptWarning,
analyzeBootstrapBudget,
buildBootstrapInjectionStats,
buildBootstrapPromptWarning,
} from "../../../src/agents/bootstrap-budget.js";
import { resolveBootstrapContextForRun } from "../../../src/agents/bootstrap-files.js";
import { buildCurrentInboundPrompt } from "../../../src/agents/embedded-agent-runner/run/runtime-context-prompt.js";