diff --git a/src/agents/agent-tool-metadata.ts b/src/agents/agent-tool-metadata.ts new file mode 100644 index 000000000000..7c719c9a4e42 --- /dev/null +++ b/src/agents/agent-tool-metadata.ts @@ -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(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; +} diff --git a/src/agents/agent-tools.abort.ts b/src/agents/agent-tools.abort.ts index 41647309707f..e31275799620 100644 --- a/src/agents/agent-tools.abort.ts +++ b/src/agents/agent-tools.abort.ts @@ -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); } diff --git a/src/agents/agent-tools.before-tool-call.ts b/src/agents/agent-tools.before-tool-call.ts index 84db9361ae00..15f3b5e3dec5 100644 --- a/src/agents/agent-tools.before-tool-call.ts +++ b/src/agents/agent-tools.before-tool-call.ts @@ -18,7 +18,6 @@ export { peekAdjustedParamsForToolCall, } from "./agent-tools.before-tool-call.state.js"; export { - copyBeforeToolCallHookMarker, isToolWrappedWithBeforeToolCallHook, setBeforeToolCallDiagnosticsEnabled, } from "./before-tool-call-metadata.js"; diff --git a/src/agents/agent-tools.deferred-followup.ts b/src/agents/agent-tools.deferred-followup.ts index 552806d946dc..d987502bbc05 100644 --- a/src/agents/agent-tools.deferred-followup.ts +++ b/src/agents/agent-tools.deferred-followup.ts @@ -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. */ diff --git a/src/agents/agent-tools.runtime.test.ts b/src/agents/agent-tools.runtime.test.ts index ce13a0b0c18e..f56ea17a8023 100644 --- a/src/agents/agent-tools.runtime.test.ts +++ b/src/agents/agent-tools.runtime.test.ts @@ -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; @@ -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", () => { diff --git a/src/agents/agent-tools.schema.ts b/src/agents/agent-tools.schema.ts index a6067cb51efa..0877b974cae1 100644 --- a/src/agents/agent-tools.schema.ts +++ b/src/agents/agent-tools.schema.ts @@ -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) @@ -84,7 +74,7 @@ export function normalizeToolParameters( return tool; } const parameters = normalizeToolParameterSchema(schema, options); - return preserveToolMeta({ + return copyAgentToolMetadata(tool, { ...tool, ...addEmptyObjectArgumentPreparation(tool, parameters), parameters, diff --git a/src/agents/bootstrap-budget-warning.ts b/src/agents/bootstrap-budget-warning.ts new file mode 100644 index 000000000000..1083022d5e95 --- /dev/null +++ b/src/agents/bootstrap-budget-warning.ts @@ -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(); + 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()); + 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, + }; +} diff --git a/src/agents/bootstrap-budget.test.ts b/src/agents/bootstrap-budget.test.ts index 6c1a0a0ad9d1..6f980b269ee0 100644 --- a/src/agents/bootstrap-budget.test.ts +++ b/src/agents/bootstrap-budget.test.ts @@ -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[] = [ diff --git a/src/agents/bootstrap-budget.ts b/src/agents/bootstrap-budget.ts index 98518ebd2951..7d927ab85648 100644 --- a/src/agents/bootstrap-budget.ts +++ b/src/agents/bootstrap-budget.ts @@ -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(); - 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()); - 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, }; } diff --git a/src/agents/bootstrap-budget.types.ts b/src/agents/bootstrap-budget.types.ts new file mode 100644 index 000000000000..56ac486acba5 --- /dev/null +++ b/src/agents/bootstrap-budget.types.ts @@ -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[]; +}; diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 14f23e23e8db..976ae3ed93d7 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -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, }); diff --git a/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts b/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts index 09e345d3faba..c5912fb617ec 100644 --- a/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts +++ b/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts @@ -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 => ({ 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); - }); -}); diff --git a/src/agents/embedded-agent-helpers.ts b/src/agents/embedded-agent-helpers.ts index 98b599e2c5cb..9b2d13f12c1d 100644 --- a/src/agents/embedded-agent-helpers.ts +++ b/src/agents/embedded-agent-helpers.ts @@ -3,7 +3,6 @@ export { buildBootstrapContextFiles, resolveBootstrapMaxChars, - resolveBootstrapPromptTruncationWarningMode, resolveBootstrapTotalMaxChars, } from "./embedded-agent-helpers/bootstrap.js"; export { diff --git a/src/agents/embedded-agent-helpers/bootstrap.ts b/src/agents/embedded-agent-helpers/bootstrap.ts index 08ba2dc88784..f791db8f9756 100644 --- a/src/agents/embedded-agent-helpers/bootstrap.ts +++ b/src/agents/embedded-agent-helpers/bootstrap.ts @@ -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(); } diff --git a/src/agents/embedded-agent-runner/run/attempt-bootstrap-prepare.ts b/src/agents/embedded-agent-runner/run/attempt-bootstrap-prepare.ts index e282bf5a8a9a..894fe99350ed 100644 --- a/src/agents/embedded-agent-runner/run/attempt-bootstrap-prepare.ts +++ b/src/agents/embedded-agent-runner/run/attempt-bootstrap-prepare.ts @@ -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, diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts index ba1f73a38a68..dc1da04fe8de 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts @@ -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"; diff --git a/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts b/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts index 996ea9f14d41..130b51accec9 100644 --- a/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts +++ b/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts @@ -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 = { 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.for("openclaw:beforeToolCallWrapped")]).toBe( - true, - ); + expect(isToolWrappedWithBeforeToolCallHook(wrapped as never)).toBe(true); + expect((wrapped as Record)[BEFORE_TOOL_CALL_SOURCE_TOOL]).toBe(sourceTool); }); it("preserves terminal presentation metadata on heartbeat-wrapped tools", () => { diff --git a/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.ts b/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.ts index 10a50a2ffc84..805c4db6d9bc 100644 --- a/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.ts +++ b/src/agents/embedded-agent-runner/run/tool-activity-heartbeat.ts @@ -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( } }) 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); } diff --git a/src/agents/runtime-plan/tools.ts b/src/agents/runtime-plan/tools.ts index 89c64c40d176..9e7959d48198 100644 --- a/src/agents/runtime-plan/tools.ts +++ b/src/agents/runtime-plan/tools.ts @@ -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 diff --git a/src/agents/tools/gateway-caller-context.ts b/src/agents/tools/gateway-caller-context.ts index b70a4a472b7a..b56504d6aeff 100644 --- a/src/agents/tools/gateway-caller-context.ts +++ b/src/agents/tools/gateway-caller-context.ts @@ -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( diff --git a/src/auto-reply/reply/dispatch-from-config.choose-route.ts b/src/auto-reply/reply/dispatch-from-config.choose-route.ts index bd6ced05f927..2567fd66773e 100644 --- a/src/auto-reply/reply/dispatch-from-config.choose-route.ts +++ b/src/auto-reply/reply/dispatch-from-config.choose-route.ts @@ -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(); const routeState: { sessionMetadataChangesForResult?: CommandSessionMetadataChange[] } = {}; const notifySessionMetadataChanges = ( @@ -600,7 +599,6 @@ export async function chooseDispatchRoute(state: PrepareDispatchOperationReadySt shouldSuppressDefaultToolProgressMessages, shouldSendVerboseProgressMessages, shouldSendToolSummaries, - shouldSendToolStartStatuses, notifySessionMetadataChanges, shouldDeliverVerboseProgressDespiteSourceSuppression, shouldDeliverForcedToolProgressDespiteSourceSuppression, diff --git a/src/auto-reply/reply/dispatch-from-config.execute.ts b/src/auto-reply/reply/dispatch-from-config.execute.ts index 38f5aad79566..8365af58fc98 100644 --- a/src/auto-reply/reply/dispatch-from-config.execute.ts +++ b/src/auto-reply/reply/dispatch-from-config.execute.ts @@ -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(); diff --git a/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts b/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts index bf81696ba8eb..caee75b92301 100644 --- a/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts +++ b/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts @@ -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(); - 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 => { - 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, diff --git a/test/helpers/agents/prompt-composition-scenarios.ts b/test/helpers/agents/prompt-composition-scenarios.ts index 0407a59fd957..cca65b4546fd 100644 --- a/test/helpers/agents/prompt-composition-scenarios.ts +++ b/test/helpers/agents/prompt-composition-scenarios.ts @@ -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";