fix(agent): fix system prompt reload and tool casing issues

- reloadSystemPrompt() now updates both agent and session prompt
- reloadSystemPrompt() uses buildSkillsPrompt() consistently (not buildModelSkillsPrompt)
- Extract rebuildSystemPrompt() to eliminate duplicated builder logic
- Preserve original tool name casing in tooling summary (matching OpenClaw)
- Fix report line count to use actual newline count

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-04 15:46:18 +08:00
parent 8333615fb4
commit f733f0bcc3
4 changed files with 67 additions and 52 deletions

View file

@ -588,36 +588,11 @@ export class Agent {
return options.systemPrompt;
}
const profile = this.profile?.getProfile();
if (!profile && !options.profileId) {
if (!this.profile?.getProfile() && !options.profileId) {
return undefined;
}
const mode: SystemPromptMode = options.isSubagent ? "minimal" : "full";
const skillsPrompt = this.skillManager?.buildSkillsPrompt();
const runtime = collectRuntimeInfo({
agentName: this.profile?.getName(),
provider: this.resolvedProvider,
model: this.agent.state.model?.id,
});
return buildStructuredSystemPrompt({
mode,
profile: profile
? {
soul: profile.soul,
user: profile.user,
workspace: profile.workspace,
memory: profile.memory,
config: profile.config,
}
: undefined,
profileDir: this.profile?.getProfileDir(),
tools: toolNames,
skillsPrompt,
runtime,
});
return this.rebuildSystemPrompt(toolNames);
}
/**
@ -631,36 +606,44 @@ export class Agent {
this.profile.reloadProfile();
// Rebuild using current tool names
const toolNames = (this.agent.state.tools ?? []).map((t: { name: string }) => t.name);
const skillsPrompt = this.skillManager?.buildModelSkillsPrompt();
const systemPrompt = this.rebuildSystemPrompt(toolNames);
if (systemPrompt) {
this.agent.setSystemPrompt(systemPrompt);
this.session.setSystemPrompt(systemPrompt);
}
}
/**
* Rebuild system prompt from current state.
* Shared by constructor (via buildFullSystemPrompt) and reloadSystemPrompt.
*/
private rebuildSystemPrompt(toolNames: string[]): string | undefined {
const profile = this.profile?.getProfile();
if (!profile) return undefined;
const skillsPrompt = this.skillManager?.buildSkillsPrompt();
const runtime = collectRuntimeInfo({
agentName: this.profile.getName(),
agentName: this.profile?.getName(),
provider: this.resolvedProvider,
model: this.agent.state.model?.id,
});
const profile = this.profile.getProfile();
const systemPrompt = buildStructuredSystemPrompt({
return buildStructuredSystemPrompt({
mode: "full",
profile: profile
? {
soul: profile.soul,
user: profile.user,
workspace: profile.workspace,
memory: profile.memory,
config: profile.config,
}
: undefined,
profileDir: this.profile.getProfileDir(),
profile: {
soul: profile.soul,
user: profile.user,
workspace: profile.workspace,
memory: profile.memory,
config: profile.config,
},
profileDir: this.profile!.getProfileDir(),
tools: toolNames,
skillsPrompt,
runtime,
});
if (systemPrompt) {
this.agent.setSystemPrompt(systemPrompt);
}
}
}

View file

@ -79,7 +79,7 @@ export function buildSystemPromptWithReport(options: SystemPromptOptions): {
reportSections.push({
name,
chars: content.length,
lines: lines.length,
lines: included ? content.split("\n").length : 0,
included,
});
if (included) {

View file

@ -120,6 +120,24 @@ describe("buildToolingSummary", () => {
const result = buildToolingSummary(["read"], "minimal");
expect(result.join("\n")).toContain("## Tooling");
});
it("preserves original tool casing", () => {
const result = buildToolingSummary(["Read", "MyCustomTool", "EXEC"], "full");
const text = result.join("\n");
// Core tools: first-seen casing preserved
expect(text).toContain("- Read: Read file contents");
expect(text).toContain("- EXEC: Run shell commands");
// Unknown tools: original casing preserved
expect(text).toContain("- MyCustomTool");
});
it("deduplicates tools by lowercase", () => {
const result = buildToolingSummary(["read", "Read", "READ"], "full");
const text = result.join("\n");
// Should appear only once (first occurrence)
const matches = text.match(/- read/gi);
expect(matches).toHaveLength(1);
});
});
describe("buildToolCallStyleSection", () => {

View file

@ -113,6 +113,7 @@ export function buildSafetySection(includeSafety: boolean): string[] {
/**
* Tooling summary lists active tools with descriptions.
* Included in full and minimal modes.
* Preserves original tool casing while deduplicating by lowercase.
*/
export function buildToolingSummary(
tools: string[] | undefined,
@ -120,7 +121,18 @@ export function buildToolingSummary(
): string[] {
if (mode === "none" || !tools || tools.length === 0) return [];
const toolSet = new Set(tools.map((t) => t.toLowerCase()));
// Preserve original casing: first occurrence wins per normalized name
const canonicalByNormalized = new Map<string, string>();
for (const name of tools) {
const normalized = name.toLowerCase();
if (!canonicalByNormalized.has(normalized)) {
canonicalByNormalized.set(normalized, name);
}
}
const resolveToolName = (normalized: string) =>
canonicalByNormalized.get(normalized) ?? normalized;
const normalizedTools = new Set(canonicalByNormalized.keys());
// Build ordered tool lines
const toolLines: string[] = [];
@ -128,17 +140,19 @@ export function buildToolingSummary(
// Core tools in preferred order
for (const tool of TOOL_ORDER) {
if (toolSet.has(tool) && !seen.has(tool)) {
if (normalizedTools.has(tool) && !seen.has(tool)) {
seen.add(tool);
const displayName = resolveToolName(tool);
const summary = CORE_TOOL_SUMMARIES[tool];
toolLines.push(summary ? `- ${tool}: ${summary}` : `- ${tool}`);
toolLines.push(summary ? `- ${displayName}: ${summary}` : `- ${displayName}`);
}
}
// External/unknown tools alphabetically
const extraTools = [...toolSet].filter((t) => !seen.has(t)).sort();
const extraTools = [...normalizedTools].filter((t) => !seen.has(t)).sort();
for (const tool of extraTools) {
toolLines.push(`- ${tool}`);
const displayName = resolveToolName(tool);
toolLines.push(`- ${displayName}`);
}
return [