refactor(agents): use structured tool definitions

This commit is contained in:
Peter Steinberger
2026-04-05 13:26:15 +01:00
parent 7343d1b2ad
commit 1dc3da6eda
10 changed files with 32 additions and 166 deletions

View File

@@ -4,7 +4,6 @@ import type { MemoryCitationsMode } from "../../config/types.memory.js";
import type { ResolvedTimeFormat } from "../date-time.js";
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { buildAgentSystemPrompt, type PromptMode } from "../system-prompt.js";
import { buildToolSummaryMap } from "../tool-summaries.js";
import type { EmbeddedSandboxInfo } from "./types.js";
import type { ReasoningLevel, ThinkLevel } from "./utils.js";
@@ -74,7 +73,6 @@ export function buildEmbeddedSystemPrompt(params: {
messageToolHints: params.messageToolHints,
sandboxInfo: params.sandboxInfo,
toolNames: params.tools.map((tool) => tool.name),
toolSummaries: buildToolSummaryMap(params.tools),
modelAliasLines: params.modelAliasLines,
userTimezone: params.userTimezone,
userTime: params.userTime,

View File

@@ -19,7 +19,6 @@ import { resolveBootstrapContextForRun } from "./bootstrap-files.js";
import { buildEmbeddedSystemPrompt } from "./pi-embedded-runner/system-prompt.js";
import { buildAgentSystemPrompt } from "./system-prompt.js";
import { createStubTool } from "./test-helpers/pi-tool-stubs.js";
import { buildToolSummaryMap } from "./tool-summaries.js";
export type PromptScenarioTurn = {
id: string;
@@ -50,9 +49,6 @@ function buildCommonSystemParams(workspaceDir: string) {
"x_search",
"web_fetch",
];
const toolSummaries = buildToolSummaryMap(
toolNames.map((name) => ({ name, description: `${name} tool` }) as never),
);
return {
runtimeInfo: {
agentId: "main",
@@ -69,7 +65,6 @@ function buildCommonSystemParams(workspaceDir: string) {
userTime: "Monday, March 16th, 2026 — 9:00 PM",
userTimeFormat: "12" as const,
toolNames,
toolSummaries,
};
}
@@ -80,7 +75,7 @@ function buildSystemPrompt(params: {
reactionGuidance?: { level: "minimal" | "extensive"; channel: string };
contextFiles?: Array<{ path: string; content: string }>;
}) {
const { runtimeInfo, userTimezone, userTime, userTimeFormat, toolNames, toolSummaries } =
const { runtimeInfo, userTimezone, userTime, userTimeFormat, toolNames } =
buildCommonSystemParams(params.workspaceDir);
return buildAgentSystemPrompt({
workspaceDir: params.workspaceDir,
@@ -90,7 +85,6 @@ function buildSystemPrompt(params: {
userTime,
userTimeFormat,
toolNames,
toolSummaries,
modelAliasLines: [],
promptMode: "full",
acpEnabled: true,

View File

@@ -82,6 +82,17 @@ describe("buildSystemPromptReport", () => {
expect(report.bootstrapTotalMaxChars).toBe(22_222);
});
it("reports zero in-band tool list chars when tool info stays structured", () => {
const file = makeBootstrapFile({ path: "/tmp/workspace/policies/AGENTS.md" });
const report = makeReport({
file,
injectedPath: "AGENTS.md",
injectedContent: "trimmed",
});
expect(report.tools.listChars).toBe(0);
});
it("reports injectedChars=0 when injected file does not match by path or basename", () => {
const file = makeBootstrapFile({ path: "/tmp/workspace/policies/AGENTS.md" });
const report = makeReport({

View File

@@ -66,17 +66,6 @@ function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools
});
}
function extractToolListText(systemPrompt: string): string {
const markerA = "Tool names are case-sensitive. Call tools exactly as listed.\n";
const markerB =
"\nTOOLS.md does not control tool availability; it is user guidance for how to use external tools.";
const extracted = extractBetween(systemPrompt, markerA, markerB);
if (!extracted.found) {
return "";
}
return extracted.text.replace(markerA, "").trim();
}
export function buildSystemPromptReport(params: {
source: SessionSystemPromptReport["source"];
generatedAt: number;
@@ -102,8 +91,6 @@ export function buildSystemPromptReport(params: {
"\n## Silent Replies\n",
);
const projectContextChars = projectContext.text.length;
const toolListText = extractToolListText(systemPrompt);
const toolListChars = toolListText.length;
const toolsEntries = buildToolsEntries(params.tools);
const toolsSchemaChars = toolsEntries.reduce((sum, t) => sum + (t.schemaChars ?? 0), 0);
const skillsEntries = parseSkillBlocks(params.skillsPrompt);
@@ -134,7 +121,7 @@ export function buildSystemPromptReport(params: {
entries: skillsEntries,
},
tools: {
listChars: toolListChars,
listChars: 0,
schemaChars: toolsSchemaChars,
entries: toolsEntries,
},

View File

@@ -339,16 +339,25 @@ describe("buildAgentSystemPrompt", () => {
);
});
it("lists available tools when provided", () => {
it("uses structured tool definitions as the source of truth", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["exec", "sessions_list", "sessions_history", "sessions_send"],
});
expect(prompt).toContain("Tool availability (filtered by policy):");
expect(prompt).toContain("sessions_list");
expect(prompt).toContain("sessions_history");
expect(prompt).toContain("sessions_send");
expect(prompt).toContain(
"Structured tool definitions are the source of truth for tool names, descriptions, and parameters.",
);
expect(prompt).toContain(
"Tool names are case-sensitive. Call tools exactly as listed in the structured tool definitions.",
);
expect(prompt).toContain(
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
);
expect(prompt).not.toContain("Tool availability (filtered by policy):");
expect(prompt).not.toContain("- sessions_list:");
expect(prompt).not.toContain("- sessions_history:");
expect(prompt).not.toContain("- sessions_send:");
});
it("documents ACP sessions_spawn agent targeting requirements", () => {

View File

@@ -296,7 +296,6 @@ export function buildAgentSystemPrompt(params: {
ownerDisplaySecret?: string;
reasoningTagHint?: boolean;
toolNames?: string[];
toolSummaries?: Record<string, string>;
modelAliasLines?: string[];
userTimezone?: string;
userTime?: string;
@@ -337,76 +336,6 @@ export function buildAgentSystemPrompt(params: {
const acpEnabled = params.acpEnabled !== false;
const sandboxedRuntime = params.sandboxInfo?.enabled === true;
const acpSpawnRuntimeEnabled = acpEnabled && !sandboxedRuntime;
const execToolSummary =
"Run shell commands (pty available for TTY-required CLIs; use for work that starts now, not delayed follow-ups; background completion may wake automatically when enabled)";
const processToolSummary =
"Manage background exec sessions for commands already started (poll/log for inspection, debugging, input, intervention, or completion confirmation when auto-wake is unavailable)";
const cronToolSummary =
"Manage cron jobs and wake events (use for reminders, delayed follow-ups, and recurring tasks; for requests like 'check back in 10 minutes' or 'remind me later', use cron instead of exec sleep, yieldMs delays, or process polling; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)";
const coreToolSummaries: Record<string, string> = {
read: "Read file contents",
write: "Create or overwrite files",
edit: "Make precise edits to files",
apply_patch: "Apply multi-file patches",
grep: "Search file contents for patterns",
find: "Find files by glob pattern",
ls: "List directory contents",
exec: execToolSummary,
process: processToolSummary,
web_search: "Search the web",
web_fetch: "Fetch and extract readable content from a URL",
// Channel docking: add login tools here when a channel needs interactive linking.
browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas",
nodes: "List/describe/notify/camera/screen on paired nodes",
cron: cronToolSummary,
message: "Send messages and channel actions",
gateway: "Restart, apply config, or run updates on the running OpenClaw process",
agents_list: acpSpawnRuntimeEnabled
? 'List OpenClaw agent ids allowed for sessions_spawn when runtime="subagent" (not ACP harness ids)'
: "List OpenClaw agent ids allowed for sessions_spawn",
sessions_list: "List other sessions (incl. sub-agents) with filters/last",
sessions_history: "Fetch history for another session/sub-agent",
sessions_send: "Send a message to another session/sub-agent",
sessions_spawn: acpSpawnRuntimeEnabled
? 'Spawn an isolated sub-agent or ACP coding session (runtime="acp" requires `agentId` unless `acp.defaultAgent` is configured; ACP harness ids follow acp.allowedAgents, not agents_list)'
: "Spawn an isolated sub-agent session",
subagents: "List, steer, or kill sub-agent runs for this requester session",
session_status:
"Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
image: "Analyze an image with the configured image model",
image_generate: "Generate images with the configured image-generation model",
};
const toolOrder = [
"read",
"write",
"edit",
"apply_patch",
"grep",
"find",
"ls",
"exec",
"process",
"code_execution",
"web_search",
"web_fetch",
"browser",
"canvas",
"nodes",
"cron",
"message",
"gateway",
"agents_list",
"sessions_list",
"sessions_history",
"sessions_send",
"subagents",
"session_status",
"image",
"image_generate",
];
const rawToolNames = (params.toolNames ?? []).map((tool) => tool.trim());
const canonicalToolNames = rawToolNames.filter(Boolean);
// Preserve caller casing while deduping tool names by lowercase.
@@ -424,32 +353,8 @@ export function buildAgentSystemPrompt(params: {
const availableTools = new Set(normalizedTools);
const hasSessionsSpawn = availableTools.has("sessions_spawn");
const acpHarnessSpawnAllowed = hasSessionsSpawn && acpSpawnRuntimeEnabled;
const externalToolSummaries = new Map<string, string>();
for (const [key, value] of Object.entries(params.toolSummaries ?? {})) {
const normalized = key.trim().toLowerCase();
if (!normalized || !value?.trim()) {
continue;
}
externalToolSummaries.set(normalized, value.trim());
}
const extraTools = Array.from(
new Set(normalizedTools.filter((tool) => !toolOrder.includes(tool))),
);
const enabledTools = toolOrder.filter((tool) => availableTools.has(tool));
const toolLines = enabledTools.map((tool) => {
const summary = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
const name = resolveToolName(tool);
return summary ? `- ${name}: ${summary}` : `- ${name}`;
});
for (const tool of extraTools.toSorted()) {
const summary = coreToolSummaries[tool] ?? externalToolSummaries.get(tool);
const name = resolveToolName(tool);
toolLines.push(summary ? `- ${name}: ${summary}` : `- ${name}`);
}
const usingDefaultToolFallback = toolLines.length === 0;
const hasGateway = availableTools.has("gateway");
const hasCronTool = availableTools.has("cron") || usingDefaultToolFallback;
const hasCronTool = availableTools.has("cron") || canonicalToolNames.length === 0;
const readToolName = resolveToolName("read");
const execToolName = resolveToolName("exec");
const processToolName = resolveToolName("process");
@@ -545,28 +450,9 @@ export function buildAgentSystemPrompt(params: {
"You are a personal assistant running inside OpenClaw.",
"",
"## Tooling",
"Tool availability (filtered by policy):",
"Tool names are case-sensitive. Call tools exactly as listed.",
toolLines.length > 0
? toolLines.join("\n")
: [
"Pi lists the standard tools above. This runtime enables:",
"- grep: search file contents for patterns",
"- find: find files by glob pattern",
"- ls: list directory contents",
"- apply_patch: apply multi-file patches",
`- ${execToolName}: ${execToolSummary.toLowerCase()}`,
`- ${processToolName}: ${processToolSummary.toLowerCase()}`,
"- browser: control OpenClaw's dedicated browser",
"- canvas: present/eval/snapshot the Canvas",
"- nodes: list/describe/notify/camera/screen on paired nodes",
`- cron: ${cronToolSummary.toLowerCase()}`,
"- sessions_list: list sessions",
"- sessions_history: fetch session history",
"- sessions_send: send to another session",
"- subagents: list/steer/kill sub-agent runs",
'- session_status: show usage/time/model state and answer "what model are we using?"',
].join("\n"),
"Structured tool definitions are the source of truth for tool names, descriptions, and parameters.",
"Tool names are case-sensitive. Call tools exactly as listed in the structured tool definitions.",
"If a tool is present in the structured tool definitions, it is available unless a later tool call reports a policy/runtime restriction.",
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
...(hasCronTool
? [

View File

@@ -1,13 +0,0 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
export function buildToolSummaryMap(tools: AgentTool[]): Record<string, string> {
const summaries: Record<string, string> = {};
for (const tool of tools) {
const summary = tool.description?.trim() || tool.label?.trim();
if (!summary) {
continue;
}
summaries[tool.name.toLowerCase()] = summary;
}
return summaries;
}

View File

@@ -45,10 +45,6 @@ vi.mock("../../agents/system-prompt.js", () => ({
buildAgentSystemPrompt: vi.fn(() => "system prompt"),
}));
vi.mock("../../agents/tool-summaries.js", () => ({
buildToolSummaryMap: vi.fn(() => ({})),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn(() => false),
}));

View File

@@ -9,7 +9,6 @@ import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
@@ -77,7 +76,6 @@ export async function resolveCommandsSystemPromptBundle(
return [];
}
})();
const toolSummaries = buildToolSummaryMap(tools);
const toolNames = tools.map((t) => t.name);
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.cfg,
@@ -119,7 +117,6 @@ export async function resolveCommandsSystemPromptBundle(
ownerNumbers: undefined,
reasoningTagHint: false,
toolNames,
toolSummaries,
modelAliasLines: [],
userTimezone,
userTime,