mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
perf: trace reply tool prep stages
This commit is contained in:
@@ -100,6 +100,8 @@ export function createOpenClawTools(
|
||||
enableHeartbeatTool?: boolean;
|
||||
/** If true, skip plugin tool resolution and return only shipped core tools. */
|
||||
disablePluginTools?: boolean;
|
||||
/** Records hot-path tool-prep stages for reply startup diagnostics. */
|
||||
recordToolPrepStage?: (name: string) => void;
|
||||
/** Trusted sender id from inbound context (not tool args). */
|
||||
requesterSenderId?: string | null;
|
||||
/** Whether the requesting sender is an owner. */
|
||||
@@ -135,6 +137,7 @@ export function createOpenClawTools(
|
||||
const spawnWorkspaceDir = resolveWorkspaceRoot(
|
||||
options?.spawnWorkspaceDir ?? options?.workspaceDir ?? inferredWorkspaceDir,
|
||||
);
|
||||
options?.recordToolPrepStage?.("openclaw-tools:session-workspace");
|
||||
const deliveryContext = normalizeDeliveryContext({
|
||||
channel: options?.agentChannel,
|
||||
to: options?.agentTo,
|
||||
@@ -156,6 +159,7 @@ export function createOpenClawTools(
|
||||
modelHasVision: options?.modelHasVision,
|
||||
})
|
||||
: null;
|
||||
options?.recordToolPrepStage?.("openclaw-tools:image-tool");
|
||||
const imageGenerateTool = createImageGenerateTool({
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
@@ -163,6 +167,7 @@ export function createOpenClawTools(
|
||||
sandbox,
|
||||
fsPolicy: options?.fsPolicy,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:image-generate-tool");
|
||||
const videoGenerateTool = createVideoGenerateTool({
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
@@ -172,6 +177,7 @@ export function createOpenClawTools(
|
||||
sandbox,
|
||||
fsPolicy: options?.fsPolicy,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:video-generate-tool");
|
||||
const musicGenerateTool = createMusicGenerateTool({
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
@@ -181,6 +187,7 @@ export function createOpenClawTools(
|
||||
sandbox,
|
||||
fsPolicy: options?.fsPolicy,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:music-generate-tool");
|
||||
const pdfTool = options?.agentDir?.trim()
|
||||
? createPdfTool({
|
||||
config: options?.config,
|
||||
@@ -190,17 +197,20 @@ export function createOpenClawTools(
|
||||
fsPolicy: options?.fsPolicy,
|
||||
})
|
||||
: null;
|
||||
options?.recordToolPrepStage?.("openclaw-tools:pdf-tool");
|
||||
const webSearchTool = createWebSearchTool({
|
||||
config: options?.config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebSearch: runtimeWebTools?.search,
|
||||
lateBindRuntimeConfig: true,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:web-search-tool");
|
||||
const webFetchTool = createWebFetchTool({
|
||||
config: options?.config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebFetch: runtimeWebTools?.fetch,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:web-fetch-tool");
|
||||
const messageTool = options?.disableMessageTool
|
||||
? null
|
||||
: createMessageTool({
|
||||
@@ -220,6 +230,7 @@ export function createOpenClawTools(
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
});
|
||||
const heartbeatTool = options?.enableHeartbeatTool ? createHeartbeatResponseTool() : null;
|
||||
options?.recordToolPrepStage?.("openclaw-tools:message-tool");
|
||||
const nodesToolBase = createNodesTool({
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentChannel: options?.agentChannel,
|
||||
@@ -236,6 +247,7 @@ export function createOpenClawTools(
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
workspaceDir,
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:nodes-tool");
|
||||
const embedded = isEmbeddedMode();
|
||||
const effectiveCallGateway = embedded
|
||||
? createEmbeddedCallGateway()
|
||||
@@ -341,6 +353,7 @@ export function createOpenClawTools(
|
||||
}),
|
||||
...collectPresentOpenClawTools([webSearchTool, webFetchTool, imageTool, pdfTool]),
|
||||
];
|
||||
options?.recordToolPrepStage?.("openclaw-tools:core-tool-list");
|
||||
|
||||
if (options?.disablePluginTools) {
|
||||
return tools;
|
||||
@@ -351,6 +364,7 @@ export function createOpenClawTools(
|
||||
resolvedConfig,
|
||||
existingToolNames: new Set(tools.map((tool) => tool.name)),
|
||||
});
|
||||
options?.recordToolPrepStage?.("openclaw-tools:plugin-tools");
|
||||
|
||||
return [...tools, ...wrappedPluginTools];
|
||||
}
|
||||
|
||||
@@ -723,6 +723,30 @@ export async function runEmbeddedAttempt(
|
||||
log.trace(message);
|
||||
}
|
||||
};
|
||||
const emitCorePluginToolStageSummary = (
|
||||
phase: string,
|
||||
summary: ReturnType<typeof prepStages.snapshot>,
|
||||
) => {
|
||||
if (summary.stages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const shouldWarn = shouldWarnEmbeddedRunStageSummary(summary, {
|
||||
totalThresholdMs: 5_000,
|
||||
stageThresholdMs: 2_000,
|
||||
});
|
||||
if (!shouldWarn && !log.isEnabled("trace")) {
|
||||
return;
|
||||
}
|
||||
const message = formatEmbeddedRunStageSummary(
|
||||
`[trace:embedded-run] core-plugin-tool stages: runId=${params.runId} sessionId=${params.sessionId} phase=${phase}`,
|
||||
summary,
|
||||
);
|
||||
if (shouldWarn) {
|
||||
log.warn(message);
|
||||
} else {
|
||||
log.trace(message);
|
||||
}
|
||||
};
|
||||
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
|
||||
@@ -833,6 +857,7 @@ export async function runEmbeddedAttempt(
|
||||
...(err ? { errorCategory: diagnosticErrorCategory(err) } : {}),
|
||||
});
|
||||
};
|
||||
const corePluginToolStages = createEmbeddedRunStageTracker();
|
||||
const toolsRaw =
|
||||
params.disableTools || isRawModelRun
|
||||
? []
|
||||
@@ -893,6 +918,7 @@ export async function runEmbeddedAttempt(
|
||||
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
forceMessageTool: params.forceMessageTool,
|
||||
recordToolPrepStage: (name) => corePluginToolStages.mark(name),
|
||||
onYield: (message) => {
|
||||
yieldDetected = true;
|
||||
yieldMessage = message;
|
||||
@@ -901,9 +927,13 @@ export async function runEmbeddedAttempt(
|
||||
abortSessionForYield?.();
|
||||
},
|
||||
});
|
||||
return applyEmbeddedAttemptToolsAllow(allTools, params.toolsAllow);
|
||||
corePluginToolStages.mark("attempt:create-openclaw-coding-tools");
|
||||
const filteredTools = applyEmbeddedAttemptToolsAllow(allTools, params.toolsAllow);
|
||||
corePluginToolStages.mark("attempt:tools-allow");
|
||||
return filteredTools;
|
||||
})();
|
||||
prepStages.mark("core-plugin-tools");
|
||||
emitCorePluginToolStageSummary("core-plugin-tools", corePluginToolStages.snapshot());
|
||||
const toolsEnabled = supportsModelTools(params.model);
|
||||
const bootstrapHasFileAccess = toolsEnabled && toolsRaw.some((tool) => tool.name === "read");
|
||||
const bootstrapRouting = await resolveAttemptWorkspaceBootstrapRouting({
|
||||
|
||||
@@ -180,6 +180,40 @@ describe("createOpenClawCodingTools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("records core tool-prep stages for hot-path diagnostics", () => {
|
||||
const stages: string[] = [];
|
||||
|
||||
createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
recordToolPrepStage: (name) => stages.push(name),
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(stages).toEqual(
|
||||
expect.arrayContaining([
|
||||
"tool-policy",
|
||||
"workspace-policy",
|
||||
"base-coding-tools",
|
||||
"shell-tools",
|
||||
"openclaw-tools:test-helper",
|
||||
"openclaw-tools",
|
||||
"message-provider-policy",
|
||||
"model-provider-policy",
|
||||
"authorization-policy",
|
||||
"schema-normalization",
|
||||
"tool-hooks",
|
||||
"abort-wrappers",
|
||||
"deferred-followup-descriptions",
|
||||
]),
|
||||
);
|
||||
expect(stages.indexOf("tool-policy")).toBeLessThan(stages.indexOf("workspace-policy"));
|
||||
expect(stages.indexOf("workspace-policy")).toBeLessThan(stages.indexOf("base-coding-tools"));
|
||||
expect(stages.indexOf("openclaw-tools:test-helper")).toBeLessThan(
|
||||
stages.indexOf("openclaw-tools"),
|
||||
);
|
||||
expect(stages.indexOf("schema-normalization")).toBeLessThan(stages.indexOf("tool-hooks"));
|
||||
});
|
||||
|
||||
it("preserves action enums in normalized schemas", () => {
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
const toolNames = ["canvas", "nodes", "cron", "gateway", "message"];
|
||||
|
||||
@@ -489,6 +489,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
throw new Error("Sandbox filesystem bridge is unavailable.");
|
||||
}
|
||||
const imageSanitization = resolveImageSanitizationLimits(options?.config);
|
||||
options?.recordToolPrepStage?.("workspace-policy");
|
||||
|
||||
const base = includeCoreTools
|
||||
? (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||
@@ -535,6 +536,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
return [tool];
|
||||
})
|
||||
: [];
|
||||
options?.recordToolPrepStage?.("base-coding-tools");
|
||||
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
|
||||
const execTool = includeCoreTools
|
||||
? createLazyExecTool({
|
||||
@@ -594,6 +596,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
: undefined,
|
||||
workspaceOnly: applyPatchWorkspaceOnly,
|
||||
});
|
||||
options?.recordToolPrepStage?.("shell-tools");
|
||||
const pluginToolAllowlist = collectExplicitAllowlist([
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
@@ -712,9 +715,11 @@ export function createOpenClawCodingTools(options?: {
|
||||
sessionId: options?.sessionId,
|
||||
onYield: options?.onYield,
|
||||
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
|
||||
recordToolPrepStage: options?.recordToolPrepStage,
|
||||
})
|
||||
: pluginToolsOnly),
|
||||
];
|
||||
options?.recordToolPrepStage?.("openclaw-tools");
|
||||
const toolsForMemoryFlush =
|
||||
isMemoryFlushRun && memoryFlushWritePath
|
||||
? tools.flatMap((tool) => {
|
||||
@@ -741,6 +746,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
toolsForMemoryFlush,
|
||||
options?.messageProvider,
|
||||
);
|
||||
options?.recordToolPrepStage?.("message-provider-policy");
|
||||
const toolsForModelProvider = applyModelProviderToolPolicy(toolsForMessageProvider, {
|
||||
config: options?.config,
|
||||
modelProvider: options?.modelProvider,
|
||||
@@ -750,6 +756,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
modelCompat: options?.modelCompat,
|
||||
suppressManagedWebSearch: options?.suppressManagedWebSearch,
|
||||
});
|
||||
options?.recordToolPrepStage?.("model-provider-policy");
|
||||
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
||||
const senderIsOwner = options?.senderIsOwner === true;
|
||||
const toolsByAuthorization = applyOwnerOnlyToolPolicy(
|
||||
@@ -780,6 +787,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
{ policy: subagentPolicy, label: "subagent tools.allow" },
|
||||
],
|
||||
});
|
||||
options?.recordToolPrepStage?.("authorization-policy");
|
||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
||||
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||
// Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them.
|
||||
@@ -790,6 +798,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
modelCompat: options?.modelCompat,
|
||||
}),
|
||||
);
|
||||
options?.recordToolPrepStage?.("schema-normalization");
|
||||
const withHooks = normalized.map((tool) =>
|
||||
wrapToolWithBeforeToolCallHook(tool, {
|
||||
agentId,
|
||||
@@ -800,12 +809,15 @@ export function createOpenClawCodingTools(options?: {
|
||||
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
||||
}),
|
||||
);
|
||||
options?.recordToolPrepStage?.("tool-hooks");
|
||||
const withAbort = options?.abortSignal
|
||||
? withHooks.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
|
||||
: withHooks;
|
||||
options?.recordToolPrepStage?.("abort-wrappers");
|
||||
const withDeferredFollowupDescriptions = applyDeferredFollowupToolDescriptions(withAbort, {
|
||||
agentId,
|
||||
});
|
||||
options?.recordToolPrepStage?.("deferred-followup-descriptions");
|
||||
|
||||
// NOTE: Keep canonical (lowercase) tool names here.
|
||||
// pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names
|
||||
|
||||
@@ -47,10 +47,13 @@ const coreTools = [
|
||||
stubTool("pdf"),
|
||||
];
|
||||
|
||||
const createOpenClawToolsMock = vi.fn((options?: { enableHeartbeatTool?: boolean }) =>
|
||||
coreTools
|
||||
.filter((tool) => tool.name !== "heartbeat_respond" || options?.enableHeartbeatTool === true)
|
||||
.map((tool) => Object.assign({}, tool)),
|
||||
const createOpenClawToolsMock = vi.fn(
|
||||
(options?: { enableHeartbeatTool?: boolean; recordToolPrepStage?: (name: string) => void }) => {
|
||||
options?.recordToolPrepStage?.("openclaw-tools:test-helper");
|
||||
return coreTools
|
||||
.filter((tool) => tool.name !== "heartbeat_respond" || options?.enableHeartbeatTool === true)
|
||||
.map((tool) => Object.assign({}, tool));
|
||||
},
|
||||
);
|
||||
|
||||
vi.mock("../openclaw-tools.js", () => ({
|
||||
|
||||
Reference in New Issue
Block a user