feat(agent): add system prompt engineering module
Structured, mode-aware system prompt builder inspired by OpenClaw. Supports three modes (full/minimal/none), safety constitution, conditional tool sections, runtime info, and prompt telemetry. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cea3336256
commit
5a06bed549
9 changed files with 1134 additions and 0 deletions
251
src/agent/system-prompt/builder.test.ts
Normal file
251
src/agent/system-prompt/builder.test.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildSystemPrompt, buildSystemPromptWithReport } from "./builder.js";
|
||||
import type { SystemPromptOptions } from "./types.js";
|
||||
|
||||
const PROFILE = {
|
||||
soul: "# Soul\nYou are a helpful coding assistant.",
|
||||
user: "# User\nName: Alice",
|
||||
workspace: "# Workspace\nFollow conventional commits.",
|
||||
memory: "# Memory\nUser prefers TypeScript.",
|
||||
config: { name: "TestAgent" },
|
||||
};
|
||||
|
||||
const TOOLS = ["read", "write", "edit", "glob", "exec", "memory_get", "memory_set", "sessions_spawn", "web_search"];
|
||||
|
||||
describe("buildSystemPrompt", () => {
|
||||
// ── Full mode ─────────────────────────────────────────────────────────
|
||||
|
||||
it("full mode includes all profile sections", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", profile: PROFILE });
|
||||
expect(result).toContain("# Soul");
|
||||
expect(result).toContain("# User");
|
||||
expect(result).toContain("# Workspace");
|
||||
expect(result).toContain("# Memory");
|
||||
});
|
||||
|
||||
it("full mode includes safety constitution", () => {
|
||||
const result = buildSystemPrompt({ mode: "full" });
|
||||
expect(result).toContain("## Safety");
|
||||
expect(result).toContain("no independent goals");
|
||||
});
|
||||
|
||||
it("full mode includes tooling summary when tools provided", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: TOOLS });
|
||||
expect(result).toContain("## Tooling");
|
||||
expect(result).toContain("- read: Read file contents");
|
||||
expect(result).toContain("- exec: Run shell commands");
|
||||
});
|
||||
|
||||
it("full mode includes tool call style section", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: TOOLS });
|
||||
expect(result).toContain("## Tool Call Style");
|
||||
});
|
||||
|
||||
it("full mode includes memory section when memory tools present", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: ["memory_get", "memory_set"] });
|
||||
expect(result).toContain("## Memory");
|
||||
expect(result).toContain("search memory first");
|
||||
});
|
||||
|
||||
it("full mode includes sub-agents section when sessions_spawn present", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: ["sessions_spawn"] });
|
||||
expect(result).toContain("## Sub-Agents");
|
||||
});
|
||||
|
||||
it("full mode includes web access section when web tools present", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: ["web_search"] });
|
||||
expect(result).toContain("## Web Access");
|
||||
});
|
||||
|
||||
it("full mode includes skills section when provided", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "full",
|
||||
skillsPrompt: "## commit\nRun conventional commits.",
|
||||
});
|
||||
expect(result).toContain("## Skills (mandatory)");
|
||||
expect(result).toContain("## commit");
|
||||
});
|
||||
|
||||
it("full mode includes runtime info line", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "full",
|
||||
runtime: { agentName: "test", os: "darwin", arch: "arm64", nodeVersion: "v22.0.0" },
|
||||
});
|
||||
expect(result).toContain("## Runtime");
|
||||
expect(result).toContain("agent=test");
|
||||
expect(result).toContain("os=darwin (arm64)");
|
||||
});
|
||||
|
||||
it("full mode includes profile directory", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "full",
|
||||
profileDir: "/home/user/.super-multica/agent-profiles/test",
|
||||
});
|
||||
expect(result).toContain("## Profile Directory");
|
||||
expect(result).toContain("/home/user/.super-multica/agent-profiles/test");
|
||||
});
|
||||
|
||||
it("full mode excludes subagent section", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "full",
|
||||
subagent: { requesterSessionId: "a", childSessionId: "b", task: "test" },
|
||||
});
|
||||
expect(result).not.toContain("## Subagent Rules");
|
||||
});
|
||||
|
||||
// ── Minimal mode ──────────────────────────────────────────────────────
|
||||
|
||||
it("minimal mode excludes profile content", () => {
|
||||
const result = buildSystemPrompt({ mode: "minimal", profile: PROFILE });
|
||||
expect(result).not.toContain("# Soul");
|
||||
expect(result).not.toContain("# User");
|
||||
expect(result).not.toContain("# Workspace");
|
||||
expect(result).not.toContain("# Memory");
|
||||
});
|
||||
|
||||
it("minimal mode includes safety constitution", () => {
|
||||
const result = buildSystemPrompt({ mode: "minimal" });
|
||||
expect(result).toContain("## Safety");
|
||||
});
|
||||
|
||||
it("minimal mode includes tooling summary", () => {
|
||||
const result = buildSystemPrompt({ mode: "minimal", tools: ["read", "write"] });
|
||||
expect(result).toContain("## Tooling");
|
||||
});
|
||||
|
||||
it("minimal mode includes subagent context", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "minimal",
|
||||
subagent: { requesterSessionId: "parent-1", childSessionId: "child-1", task: "Search for bugs" },
|
||||
});
|
||||
expect(result).toContain("## Subagent Rules");
|
||||
expect(result).toContain("Search for bugs");
|
||||
expect(result).toContain("parent-1");
|
||||
});
|
||||
|
||||
it("minimal mode excludes skills section", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "minimal",
|
||||
skillsPrompt: "## commit\nSome skill.",
|
||||
});
|
||||
expect(result).not.toContain("## Skills");
|
||||
});
|
||||
|
||||
it("minimal mode excludes sub-agents section even with sessions_spawn", () => {
|
||||
const result = buildSystemPrompt({ mode: "minimal", tools: ["sessions_spawn"] });
|
||||
expect(result).not.toContain("## Sub-Agents");
|
||||
});
|
||||
|
||||
// ── None mode ─────────────────────────────────────────────────────────
|
||||
|
||||
it("none mode includes identity line with agent name", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "none",
|
||||
profile: { config: { name: "Multica" } },
|
||||
});
|
||||
expect(result).toContain("You are Multica, a Super Multica agent.");
|
||||
});
|
||||
|
||||
it("none mode includes safety constitution", () => {
|
||||
const result = buildSystemPrompt({ mode: "none" });
|
||||
expect(result).toContain("## Safety");
|
||||
});
|
||||
|
||||
it("none mode excludes tooling and skills", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "none",
|
||||
tools: TOOLS,
|
||||
skillsPrompt: "some skills",
|
||||
});
|
||||
expect(result).not.toContain("## Tooling");
|
||||
expect(result).not.toContain("## Skills");
|
||||
});
|
||||
|
||||
it("none mode includes subagent context", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "none",
|
||||
subagent: { requesterSessionId: "a", childSessionId: "b", task: "do stuff" },
|
||||
});
|
||||
expect(result).toContain("## Task");
|
||||
expect(result).toContain("do stuff");
|
||||
});
|
||||
|
||||
// ── Cross-cutting ─────────────────────────────────────────────────────
|
||||
|
||||
it("safety can be disabled via includeSafety=false", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", includeSafety: false });
|
||||
expect(result).not.toContain("## Safety");
|
||||
});
|
||||
|
||||
it("extra system prompt is appended", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "full",
|
||||
extraSystemPrompt: "Always respond in French.",
|
||||
});
|
||||
expect(result).toContain("## Additional Context");
|
||||
expect(result).toContain("Always respond in French.");
|
||||
});
|
||||
|
||||
it("extra system prompt uses subagent header in minimal mode", () => {
|
||||
const result = buildSystemPrompt({
|
||||
mode: "minimal",
|
||||
extraSystemPrompt: "Focus on tests.",
|
||||
});
|
||||
expect(result).toContain("## Subagent Context");
|
||||
});
|
||||
|
||||
it("returns empty-ish prompt when no options", () => {
|
||||
const result = buildSystemPrompt({ mode: "none" });
|
||||
// Should at least have identity + safety
|
||||
expect(result).toContain("Super Multica agent");
|
||||
expect(result).toContain("Safety");
|
||||
});
|
||||
|
||||
it("handles unknown tools gracefully", () => {
|
||||
const result = buildSystemPrompt({ mode: "full", tools: ["custom_tool", "read"] });
|
||||
expect(result).toContain("- read: Read file contents");
|
||||
expect(result).toContain("- custom_tool");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSystemPromptWithReport", () => {
|
||||
it("report includes accurate section counts", () => {
|
||||
const { report } = buildSystemPromptWithReport({
|
||||
mode: "full",
|
||||
profile: PROFILE,
|
||||
tools: TOOLS,
|
||||
});
|
||||
expect(report.mode).toBe("full");
|
||||
expect(report.totalChars).toBeGreaterThan(0);
|
||||
expect(report.toolCount).toBe(TOOLS.length);
|
||||
expect(report.safetyIncluded).toBe(true);
|
||||
|
||||
const identitySection = report.sections.find((s) => s.name === "identity");
|
||||
expect(identitySection?.included).toBe(true);
|
||||
|
||||
const subagentSection = report.sections.find((s) => s.name === "subagent");
|
||||
expect(subagentSection?.included).toBe(false);
|
||||
});
|
||||
|
||||
it("report reflects skills inclusion", () => {
|
||||
const { report: withSkills } = buildSystemPromptWithReport({
|
||||
mode: "full",
|
||||
skillsPrompt: "some skills",
|
||||
});
|
||||
expect(withSkills.skillsIncluded).toBe(true);
|
||||
|
||||
const { report: withoutSkills } = buildSystemPromptWithReport({
|
||||
mode: "full",
|
||||
});
|
||||
expect(withoutSkills.skillsIncluded).toBe(false);
|
||||
});
|
||||
|
||||
it("report marks excluded sections correctly in minimal mode", () => {
|
||||
const { report } = buildSystemPromptWithReport({ mode: "minimal" });
|
||||
const identity = report.sections.find((s) => s.name === "identity");
|
||||
expect(identity?.included).toBe(false);
|
||||
|
||||
const safety = report.sections.find((s) => s.name === "safety");
|
||||
expect(safety?.included).toBe(true);
|
||||
});
|
||||
});
|
||||
104
src/agent/system-prompt/builder.ts
Normal file
104
src/agent/system-prompt/builder.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* System Prompt Builder
|
||||
*
|
||||
* Core assembly logic: collects sections based on mode, filters, and joins.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PromptSection,
|
||||
SystemPromptOptions,
|
||||
SystemPromptReport,
|
||||
} from "./types.js";
|
||||
import {
|
||||
buildConditionalToolSections,
|
||||
buildExtraPromptSection,
|
||||
buildIdentitySection,
|
||||
buildMemoryFileSection,
|
||||
buildProfileDirSection,
|
||||
buildRuntimeSection,
|
||||
buildSafetySection,
|
||||
buildSkillsSection,
|
||||
buildSubagentSection,
|
||||
buildToolCallStyleSection,
|
||||
buildToolingSummary,
|
||||
buildUserSection,
|
||||
buildWorkspaceSection,
|
||||
} from "./sections.js";
|
||||
|
||||
/**
|
||||
* Build a system prompt from structured options.
|
||||
*/
|
||||
export function buildSystemPrompt(options: SystemPromptOptions): string {
|
||||
const { prompt } = buildSystemPromptWithReport(options);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a system prompt and return a diagnostic report alongside it.
|
||||
*/
|
||||
export function buildSystemPromptWithReport(options: SystemPromptOptions): {
|
||||
prompt: string;
|
||||
report: SystemPromptReport;
|
||||
} {
|
||||
const {
|
||||
mode,
|
||||
profile,
|
||||
profileDir,
|
||||
tools,
|
||||
skillsPrompt,
|
||||
runtime,
|
||||
subagent,
|
||||
extraSystemPrompt,
|
||||
includeSafety = true,
|
||||
} = options;
|
||||
|
||||
// Collect all candidate sections in order
|
||||
const candidates: Array<{ name: string; lines: string[] }> = [
|
||||
{ name: "identity", lines: buildIdentitySection(profile, mode) },
|
||||
{ name: "user", lines: buildUserSection(profile, mode) },
|
||||
{ name: "workspace", lines: buildWorkspaceSection(profile, mode) },
|
||||
{ name: "memory", lines: buildMemoryFileSection(profile, mode) },
|
||||
{ name: "safety", lines: buildSafetySection(includeSafety) },
|
||||
{ name: "tooling", lines: buildToolingSummary(tools, mode) },
|
||||
{ name: "tool-call-style", lines: buildToolCallStyleSection(mode) },
|
||||
{ name: "conditional-tools", lines: buildConditionalToolSections(tools, mode) },
|
||||
{ name: "skills", lines: buildSkillsSection(skillsPrompt, mode) },
|
||||
{ name: "runtime", lines: buildRuntimeSection(runtime, mode) },
|
||||
{ name: "profile-dir", lines: buildProfileDirSection(profileDir, mode) },
|
||||
{ name: "subagent", lines: buildSubagentSection(subagent, mode) },
|
||||
{ name: "extra", lines: buildExtraPromptSection(extraSystemPrompt, mode) },
|
||||
];
|
||||
|
||||
// Build included sections
|
||||
const sections: PromptSection[] = [];
|
||||
const reportSections: SystemPromptReport["sections"] = [];
|
||||
|
||||
for (const { name, lines } of candidates) {
|
||||
const included = lines.length > 0;
|
||||
const content = lines.join("\n");
|
||||
reportSections.push({
|
||||
name,
|
||||
chars: content.length,
|
||||
lines: lines.length,
|
||||
included,
|
||||
});
|
||||
if (included) {
|
||||
sections.push({ name, content });
|
||||
}
|
||||
}
|
||||
|
||||
// Join sections with double newline separators
|
||||
const prompt = sections.map((s) => s.content).join("\n\n");
|
||||
|
||||
const report: SystemPromptReport = {
|
||||
mode,
|
||||
totalChars: prompt.length,
|
||||
totalLines: prompt.split("\n").length,
|
||||
sections: reportSections,
|
||||
toolCount: tools?.length ?? 0,
|
||||
skillsIncluded: (skillsPrompt?.trim()?.length ?? 0) > 0 && mode === "full",
|
||||
safetyIncluded: includeSafety,
|
||||
};
|
||||
|
||||
return { prompt, report };
|
||||
}
|
||||
16
src/agent/system-prompt/constitution.ts
Normal file
16
src/agent/system-prompt/constitution.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Safety Constitution
|
||||
*
|
||||
* Always included in the system prompt regardless of mode.
|
||||
* Adapted from Anthropic's constitutional AI principles.
|
||||
*/
|
||||
|
||||
export const SAFETY_CONSTITUTION = [
|
||||
"## Safety",
|
||||
"",
|
||||
"You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.",
|
||||
"",
|
||||
"Prioritize safety and human oversight over completion; if instructions conflict, pause and ask; comply with stop/pause/audit requests and never bypass safeguards.",
|
||||
"",
|
||||
"Do not manipulate or persuade anyone to expand access or disable safeguards. Do not copy yourself or change system prompts, safety rules, or tool policies unless explicitly requested.",
|
||||
].join("\n");
|
||||
19
src/agent/system-prompt/index.ts
Normal file
19
src/agent/system-prompt/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* System Prompt Engineering — Public API
|
||||
*/
|
||||
|
||||
export { buildSystemPrompt, buildSystemPromptWithReport } from "./builder.js";
|
||||
export { collectRuntimeInfo, formatRuntimeLine } from "./runtime-info.js";
|
||||
export { formatPromptReport } from "./report.js";
|
||||
export { SAFETY_CONSTITUTION } from "./constitution.js";
|
||||
|
||||
export type {
|
||||
ProfileContent,
|
||||
PromptSection,
|
||||
RuntimeInfo,
|
||||
SectionReport,
|
||||
SubagentContext,
|
||||
SystemPromptMode,
|
||||
SystemPromptOptions,
|
||||
SystemPromptReport,
|
||||
} from "./types.js";
|
||||
29
src/agent/system-prompt/report.ts
Normal file
29
src/agent/system-prompt/report.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* System Prompt Report — telemetry and diagnostics
|
||||
*/
|
||||
|
||||
import type { SystemPromptReport } from "./types.js";
|
||||
|
||||
/**
|
||||
* Format a prompt report as a human-readable summary for debugging.
|
||||
*/
|
||||
export function formatPromptReport(report: SystemPromptReport): string {
|
||||
const lines: string[] = [
|
||||
`System Prompt Report (mode: ${report.mode})`,
|
||||
` Total: ${report.totalChars} chars, ${report.totalLines} lines`,
|
||||
` Tools: ${report.toolCount}`,
|
||||
` Skills: ${report.skillsIncluded ? "yes" : "no"}`,
|
||||
` Safety: ${report.safetyIncluded ? "yes" : "no"}`,
|
||||
"",
|
||||
" Sections:",
|
||||
];
|
||||
|
||||
for (const section of report.sections) {
|
||||
const status = section.included ? "✓" : "—";
|
||||
lines.push(
|
||||
` ${status} ${section.name}: ${section.chars} chars, ${section.lines} lines`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
48
src/agent/system-prompt/runtime-info.ts
Normal file
48
src/agent/system-prompt/runtime-info.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Runtime Info — collection and formatting
|
||||
*/
|
||||
|
||||
import os from "node:os";
|
||||
import type { RuntimeInfo } from "./types.js";
|
||||
|
||||
/**
|
||||
* Collect runtime environment information.
|
||||
* Overrides take precedence over auto-detected values.
|
||||
*/
|
||||
export function collectRuntimeInfo(overrides?: Partial<RuntimeInfo>): RuntimeInfo {
|
||||
return {
|
||||
agentName: overrides?.agentName,
|
||||
hostName: overrides?.hostName ?? os.hostname(),
|
||||
os: overrides?.os ?? process.platform,
|
||||
arch: overrides?.arch ?? process.arch,
|
||||
nodeVersion: overrides?.nodeVersion ?? process.version,
|
||||
provider: overrides?.provider,
|
||||
model: overrides?.model,
|
||||
cwd: overrides?.cwd ?? process.cwd(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format runtime info as a single-line summary.
|
||||
*
|
||||
* Example: "Runtime: agent=multica | host=macbook | os=darwin (arm64) | node=v22.0.0 | model=anthropic/claude-3.5-sonnet | cwd=/workspace"
|
||||
*/
|
||||
export function formatRuntimeLine(info: RuntimeInfo): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (info.agentName) parts.push(`agent=${info.agentName}`);
|
||||
if (info.hostName) parts.push(`host=${info.hostName}`);
|
||||
if (info.os) {
|
||||
parts.push(info.arch ? `os=${info.os} (${info.arch})` : `os=${info.os}`);
|
||||
} else if (info.arch) {
|
||||
parts.push(`arch=${info.arch}`);
|
||||
}
|
||||
if (info.nodeVersion) parts.push(`node=${info.nodeVersion}`);
|
||||
if (info.model) {
|
||||
const modelStr = info.provider ? `${info.provider}/${info.model}` : info.model;
|
||||
parts.push(`model=${modelStr}`);
|
||||
}
|
||||
if (info.cwd) parts.push(`cwd=${info.cwd}`);
|
||||
|
||||
return `Runtime: ${parts.join(" | ")}`;
|
||||
}
|
||||
245
src/agent/system-prompt/sections.test.ts
Normal file
245
src/agent/system-prompt/sections.test.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildConditionalToolSections,
|
||||
buildIdentitySection,
|
||||
buildMemoryFileSection,
|
||||
buildProfileDirSection,
|
||||
buildRuntimeSection,
|
||||
buildSafetySection,
|
||||
buildSkillsSection,
|
||||
buildSubagentSection,
|
||||
buildToolCallStyleSection,
|
||||
buildToolingSummary,
|
||||
buildUserSection,
|
||||
buildWorkspaceSection,
|
||||
} from "./sections.js";
|
||||
|
||||
describe("buildIdentitySection", () => {
|
||||
it("returns soul content in full mode", () => {
|
||||
const result = buildIdentitySection({ soul: "You are helpful." }, "full");
|
||||
expect(result).toEqual(["You are helpful."]);
|
||||
});
|
||||
|
||||
it("returns identity line with name in none mode", () => {
|
||||
const result = buildIdentitySection({ config: { name: "Cleo" } }, "none");
|
||||
expect(result).toEqual(["You are Cleo, a Super Multica agent."]);
|
||||
});
|
||||
|
||||
it("returns generic identity line in none mode without name", () => {
|
||||
const result = buildIdentitySection(undefined, "none");
|
||||
expect(result).toEqual(["You are a Super Multica agent."]);
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
const result = buildIdentitySection({ soul: "data" }, "minimal");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildUserSection", () => {
|
||||
it("returns user content in full mode", () => {
|
||||
const result = buildUserSection({ user: "Name: Bob" }, "full");
|
||||
expect(result).toEqual(["Name: Bob"]);
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
const result = buildUserSection({ user: "data" }, "minimal");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty when no user content", () => {
|
||||
const result = buildUserSection({}, "full");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildWorkspaceSection", () => {
|
||||
it("returns workspace content in full mode", () => {
|
||||
const result = buildWorkspaceSection({ workspace: "Rules here" }, "full");
|
||||
expect(result).toEqual(["Rules here"]);
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
expect(buildWorkspaceSection({ workspace: "data" }, "minimal")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildMemoryFileSection", () => {
|
||||
it("returns memory content in full mode", () => {
|
||||
const result = buildMemoryFileSection({ memory: "Key facts" }, "full");
|
||||
expect(result).toEqual(["Key facts"]);
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
expect(buildMemoryFileSection({ memory: "data" }, "minimal")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSafetySection", () => {
|
||||
it("returns safety text when enabled", () => {
|
||||
const result = buildSafetySection(true);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toContain("## Safety");
|
||||
expect(result[0]).toContain("no independent goals");
|
||||
});
|
||||
|
||||
it("returns empty when disabled", () => {
|
||||
expect(buildSafetySection(false)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildToolingSummary", () => {
|
||||
it("lists core tools with descriptions in order", () => {
|
||||
const result = buildToolingSummary(["exec", "read", "write"], "full");
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("## Tooling");
|
||||
expect(text).toContain("- read: Read file contents");
|
||||
expect(text).toContain("- write: Create or overwrite files");
|
||||
expect(text).toContain("- exec: Run shell commands");
|
||||
// read should appear before exec (order)
|
||||
expect(text.indexOf("- read")).toBeLessThan(text.indexOf("- exec"));
|
||||
});
|
||||
|
||||
it("appends unknown tools alphabetically", () => {
|
||||
const result = buildToolingSummary(["read", "zeta_tool", "alpha_tool"], "full");
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("- alpha_tool");
|
||||
expect(text).toContain("- zeta_tool");
|
||||
expect(text.indexOf("- alpha_tool")).toBeLessThan(text.indexOf("- zeta_tool"));
|
||||
});
|
||||
|
||||
it("returns empty for none mode", () => {
|
||||
expect(buildToolingSummary(["read"], "none")).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty for empty tools", () => {
|
||||
expect(buildToolingSummary([], "full")).toEqual([]);
|
||||
});
|
||||
|
||||
it("works in minimal mode", () => {
|
||||
const result = buildToolingSummary(["read"], "minimal");
|
||||
expect(result.join("\n")).toContain("## Tooling");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildToolCallStyleSection", () => {
|
||||
it("returns content in full mode", () => {
|
||||
const result = buildToolCallStyleSection("full");
|
||||
expect(result.join("\n")).toContain("## Tool Call Style");
|
||||
});
|
||||
|
||||
it("returns content in minimal mode", () => {
|
||||
expect(buildToolCallStyleSection("minimal").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("returns empty in none mode", () => {
|
||||
expect(buildToolCallStyleSection("none")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConditionalToolSections", () => {
|
||||
it("includes memory section when memory tools present", () => {
|
||||
const result = buildConditionalToolSections(["memory_get", "read"], "full");
|
||||
expect(result.join("\n")).toContain("## Memory");
|
||||
});
|
||||
|
||||
it("includes sub-agents section when sessions_spawn present in full mode", () => {
|
||||
const result = buildConditionalToolSections(["sessions_spawn"], "full");
|
||||
expect(result.join("\n")).toContain("## Sub-Agents");
|
||||
});
|
||||
|
||||
it("excludes sub-agents section in minimal mode", () => {
|
||||
const result = buildConditionalToolSections(["sessions_spawn"], "minimal");
|
||||
expect(result.join("\n")).not.toContain("## Sub-Agents");
|
||||
});
|
||||
|
||||
it("includes web access section when web tools present", () => {
|
||||
const result = buildConditionalToolSections(["web_search"], "full");
|
||||
expect(result.join("\n")).toContain("## Web Access");
|
||||
});
|
||||
|
||||
it("returns empty when no conditional tools match", () => {
|
||||
const result = buildConditionalToolSections(["read", "write"], "full");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty in none mode", () => {
|
||||
expect(buildConditionalToolSections(["memory_get"], "none")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSkillsSection", () => {
|
||||
it("wraps skills prompt in full mode", () => {
|
||||
const result = buildSkillsSection("## commit\nDo commits.", "full");
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("## Skills (mandatory)");
|
||||
expect(text).toContain("## commit");
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
expect(buildSkillsSection("skills", "minimal")).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty when skills prompt is empty", () => {
|
||||
expect(buildSkillsSection("", "full")).toEqual([]);
|
||||
expect(buildSkillsSection(undefined, "full")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRuntimeSection", () => {
|
||||
it("formats runtime info in full mode", () => {
|
||||
const result = buildRuntimeSection(
|
||||
{ agentName: "test", os: "darwin", arch: "arm64", nodeVersion: "v22.0.0", model: "claude", provider: "anthropic" },
|
||||
"full",
|
||||
);
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("## Runtime");
|
||||
expect(text).toContain("agent=test");
|
||||
expect(text).toContain("os=darwin (arm64)");
|
||||
expect(text).toContain("model=anthropic/claude");
|
||||
});
|
||||
|
||||
it("returns empty in none mode", () => {
|
||||
expect(buildRuntimeSection({ os: "darwin" }, "none")).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty when no runtime provided", () => {
|
||||
expect(buildRuntimeSection(undefined, "full")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildProfileDirSection", () => {
|
||||
it("includes path in full mode", () => {
|
||||
const result = buildProfileDirSection("/path/to/profile", "full");
|
||||
expect(result.join("\n")).toContain("/path/to/profile");
|
||||
});
|
||||
|
||||
it("returns empty in minimal mode", () => {
|
||||
expect(buildProfileDirSection("/path", "minimal")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSubagentSection", () => {
|
||||
const ctx = { requesterSessionId: "parent", childSessionId: "child", task: "Find bugs" };
|
||||
|
||||
it("returns rules and task in minimal mode", () => {
|
||||
const result = buildSubagentSection(ctx, "minimal");
|
||||
const text = result.join("\n");
|
||||
expect(text).toContain("## Subagent Rules");
|
||||
expect(text).toContain("## Task");
|
||||
expect(text).toContain("Find bugs");
|
||||
});
|
||||
|
||||
it("includes label when provided", () => {
|
||||
const result = buildSubagentSection({ ...ctx, label: "Bug Hunter" }, "minimal");
|
||||
expect(result.join("\n")).toContain('Label: "Bug Hunter"');
|
||||
});
|
||||
|
||||
it("returns empty in full mode", () => {
|
||||
expect(buildSubagentSection(ctx, "full")).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty when no subagent context", () => {
|
||||
expect(buildSubagentSection(undefined, "minimal")).toEqual([]);
|
||||
});
|
||||
});
|
||||
316
src/agent/system-prompt/sections.ts
Normal file
316
src/agent/system-prompt/sections.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
/**
|
||||
* System Prompt Section Builders
|
||||
*
|
||||
* Each function returns string[] (lines to include) or [] to skip.
|
||||
*/
|
||||
|
||||
import { SAFETY_CONSTITUTION } from "./constitution.js";
|
||||
import { formatRuntimeLine } from "./runtime-info.js";
|
||||
import type { ProfileContent, RuntimeInfo, SubagentContext, SystemPromptMode } from "./types.js";
|
||||
|
||||
// ─── Core tool summaries ────────────────────────────────────────────────────
|
||||
|
||||
/** Brief descriptions of Super Multica's built-in tools */
|
||||
const CORE_TOOL_SUMMARIES: Record<string, string> = {
|
||||
read: "Read file contents",
|
||||
write: "Create or overwrite files",
|
||||
edit: "Make precise edits to files",
|
||||
glob: "Find files by glob pattern",
|
||||
exec: "Run shell commands",
|
||||
process: "Manage background exec sessions",
|
||||
web_search: "Search the web",
|
||||
web_fetch: "Fetch and extract readable content from a URL",
|
||||
memory_get: "Read from agent memory",
|
||||
memory_set: "Write to agent memory",
|
||||
memory_list: "List memory entries",
|
||||
memory_delete: "Delete memory entries",
|
||||
sessions_spawn: "Spawn a sub-agent session",
|
||||
};
|
||||
|
||||
/** Preferred display order for tools */
|
||||
const TOOL_ORDER = [
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"glob",
|
||||
"exec",
|
||||
"process",
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
"memory_get",
|
||||
"memory_set",
|
||||
"memory_list",
|
||||
"memory_delete",
|
||||
"sessions_spawn",
|
||||
];
|
||||
|
||||
// ─── Section builders ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Identity section — soul.md in full mode, single line in none mode, nothing in minimal.
|
||||
*/
|
||||
export function buildIdentitySection(
|
||||
profile: ProfileContent | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode === "none") {
|
||||
const name = profile?.config?.name;
|
||||
return name
|
||||
? [`You are ${name}, a Super Multica agent.`]
|
||||
: ["You are a Super Multica agent."];
|
||||
}
|
||||
if (mode === "minimal") {
|
||||
return [];
|
||||
}
|
||||
// full mode
|
||||
if (profile?.soul) {
|
||||
return [profile.soul];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* User section — user.md content (full mode only).
|
||||
*/
|
||||
export function buildUserSection(
|
||||
profile: ProfileContent | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode !== "full" || !profile?.user) return [];
|
||||
return [profile.user];
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace section — workspace.md content (full mode only).
|
||||
*/
|
||||
export function buildWorkspaceSection(
|
||||
profile: ProfileContent | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode !== "full" || !profile?.workspace) return [];
|
||||
return [profile.workspace];
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory section — memory.md content (full mode only).
|
||||
*/
|
||||
export function buildMemoryFileSection(
|
||||
profile: ProfileContent | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode !== "full" || !profile?.memory) return [];
|
||||
return [profile.memory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safety constitution — always included.
|
||||
*/
|
||||
export function buildSafetySection(includeSafety: boolean): string[] {
|
||||
if (!includeSafety) return [];
|
||||
return [SAFETY_CONSTITUTION];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooling summary — lists active tools with descriptions.
|
||||
* Included in full and minimal modes.
|
||||
*/
|
||||
export function buildToolingSummary(
|
||||
tools: string[] | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode === "none" || !tools || tools.length === 0) return [];
|
||||
|
||||
const toolSet = new Set(tools.map((t) => t.toLowerCase()));
|
||||
|
||||
// Build ordered tool lines
|
||||
const toolLines: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Core tools in preferred order
|
||||
for (const tool of TOOL_ORDER) {
|
||||
if (toolSet.has(tool) && !seen.has(tool)) {
|
||||
seen.add(tool);
|
||||
const summary = CORE_TOOL_SUMMARIES[tool];
|
||||
toolLines.push(summary ? `- ${tool}: ${summary}` : `- ${tool}`);
|
||||
}
|
||||
}
|
||||
|
||||
// External/unknown tools alphabetically
|
||||
const extraTools = [...toolSet].filter((t) => !seen.has(t)).sort();
|
||||
for (const tool of extraTools) {
|
||||
toolLines.push(`- ${tool}`);
|
||||
}
|
||||
|
||||
return [
|
||||
"## Tooling",
|
||||
"Tool availability (filtered by policy):",
|
||||
"Tool names are case-sensitive. Call tools exactly as listed.",
|
||||
toolLines.join("\n"),
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call style guidance — full and minimal modes.
|
||||
*/
|
||||
export function buildToolCallStyleSection(mode: SystemPromptMode): string[] {
|
||||
if (mode === "none") return [];
|
||||
return [
|
||||
"## Tool Call Style",
|
||||
"Default: do not narrate routine, low-risk tool calls (just call the tool).",
|
||||
"Narrate only when it helps: multi-step work, complex problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
|
||||
"Keep narration brief and value-dense; avoid repeating obvious steps.",
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional tool sections — inject usage hints based on which tools are active.
|
||||
*/
|
||||
export function buildConditionalToolSections(
|
||||
tools: string[] | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode === "none" || !tools || tools.length === 0) return [];
|
||||
|
||||
const toolSet = new Set(tools.map((t) => t.toLowerCase()));
|
||||
const lines: string[] = [];
|
||||
|
||||
// Memory tools
|
||||
const hasMemory =
|
||||
toolSet.has("memory_get") ||
|
||||
toolSet.has("memory_set") ||
|
||||
toolSet.has("memory_list") ||
|
||||
toolSet.has("memory_delete");
|
||||
if (hasMemory) {
|
||||
lines.push(
|
||||
"## Memory",
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: search memory first, then pull only the needed entries.",
|
||||
"Update memory when the user shares important information, decisions, or preferences.",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Subagent tools (full mode only — minimal agents cannot spawn)
|
||||
if (mode === "full" && toolSet.has("sessions_spawn")) {
|
||||
lines.push(
|
||||
"## Sub-Agents",
|
||||
"If a task is complex or long-running, spawn a sub-agent. It will do the work and report back when done.",
|
||||
"You can check on running sub-agents at any time.",
|
||||
"Sub-agents cannot spawn nested sub-agents.",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Web tools
|
||||
if (toolSet.has("web_search") || toolSet.has("web_fetch")) {
|
||||
lines.push(
|
||||
"## Web Access",
|
||||
"You have web access. Use it when the user asks about current events, needs up-to-date information, or requests content from URLs.",
|
||||
"Prefer web_search for discovery and web_fetch for specific URLs.",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skills section — wraps SkillManager output with mandatory scan instructions.
|
||||
* Full mode only.
|
||||
*/
|
||||
export function buildSkillsSection(
|
||||
skillsPrompt: string | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode !== "full") return [];
|
||||
const trimmed = skillsPrompt?.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
return [
|
||||
"## Skills (mandatory)",
|
||||
"Before replying: scan the available skills below.",
|
||||
"- If exactly one skill clearly applies: follow its instructions.",
|
||||
"- If multiple could apply: choose the most specific one.",
|
||||
"- If none clearly apply: skip skill invocation.",
|
||||
"",
|
||||
trimmed,
|
||||
"",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime info line — full and minimal modes.
|
||||
*/
|
||||
export function buildRuntimeSection(
|
||||
runtime: RuntimeInfo | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode === "none" || !runtime) return [];
|
||||
return ["## Runtime", formatRuntimeLine(runtime)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile directory section — tells agent where its files live.
|
||||
* Full mode only.
|
||||
*/
|
||||
export function buildProfileDirSection(
|
||||
profileDir: string | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode !== "full" || !profileDir) return [];
|
||||
return [
|
||||
"## Profile Directory",
|
||||
"",
|
||||
`Your profile files are located at: \`${profileDir}\``,
|
||||
"",
|
||||
"Use `edit` or `write` tools to update these files when needed.",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subagent context — rules and task for child agents.
|
||||
* Minimal and none modes only.
|
||||
*/
|
||||
export function buildSubagentSection(
|
||||
subagent: SubagentContext | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
if (mode === "full" || !subagent) return [];
|
||||
|
||||
const lines: string[] = [
|
||||
"## Subagent Rules",
|
||||
"- Stay focused on the assigned task below.",
|
||||
"- Complete the task thoroughly and report your findings.",
|
||||
"- Do NOT initiate side actions unrelated to the task.",
|
||||
"- Do NOT attempt to communicate with the user directly.",
|
||||
"- Do NOT spawn nested subagents.",
|
||||
"- Your session is ephemeral and will be cleaned up after completion.",
|
||||
"",
|
||||
"## Context",
|
||||
`Requester session: ${subagent.requesterSessionId}`,
|
||||
`Child session: ${subagent.childSessionId}`,
|
||||
];
|
||||
|
||||
if (subagent.label) {
|
||||
lines.push(`Label: "${subagent.label}"`);
|
||||
}
|
||||
|
||||
lines.push("", "## Task", subagent.task);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra system prompt — appended at the end if provided.
|
||||
*/
|
||||
export function buildExtraPromptSection(
|
||||
extraSystemPrompt: string | undefined,
|
||||
mode: SystemPromptMode,
|
||||
): string[] {
|
||||
const trimmed = extraSystemPrompt?.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
const header = mode === "minimal" ? "## Subagent Context" : "## Additional Context";
|
||||
return [header, trimmed];
|
||||
}
|
||||
106
src/agent/system-prompt/types.ts
Normal file
106
src/agent/system-prompt/types.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* System Prompt Engineering — Type Definitions
|
||||
*
|
||||
* Provides structured, mode-aware system prompt assembly
|
||||
* inspired by OpenClaw's conditional prompt system.
|
||||
*/
|
||||
|
||||
import type { ProfileConfig } from "../profile/types.js";
|
||||
|
||||
/**
|
||||
* Controls which sections are included in the system prompt.
|
||||
* - "full": All sections (default, for main agents)
|
||||
* - "minimal": Reduced sections (safety, tooling, runtime, subagent context) — for subagents
|
||||
* - "none": Identity line + safety + subagent task only — for bare subagents
|
||||
*/
|
||||
export type SystemPromptMode = "full" | "minimal" | "none";
|
||||
|
||||
/** Runtime environment information */
|
||||
export interface RuntimeInfo {
|
||||
/** Agent display name */
|
||||
agentName?: string | undefined;
|
||||
/** Machine hostname */
|
||||
hostName?: string | undefined;
|
||||
/** LLM provider (e.g. "anthropic") */
|
||||
provider?: string | undefined;
|
||||
/** Model ID (e.g. "claude-3.5-sonnet") */
|
||||
model?: string | undefined;
|
||||
/** OS platform (e.g. "darwin") */
|
||||
os?: string | undefined;
|
||||
/** CPU architecture (e.g. "arm64") */
|
||||
arch?: string | undefined;
|
||||
/** Node.js version (e.g. "v22.0.0") */
|
||||
nodeVersion?: string | undefined;
|
||||
/** Current working directory */
|
||||
cwd?: string | undefined;
|
||||
}
|
||||
|
||||
/** Subagent context for minimal/none modes */
|
||||
export interface SubagentContext {
|
||||
/** Parent session that spawned this subagent */
|
||||
requesterSessionId: string;
|
||||
/** This subagent's session ID */
|
||||
childSessionId: string;
|
||||
/** Optional human-readable label */
|
||||
label?: string | undefined;
|
||||
/** The task this subagent must complete */
|
||||
task: string;
|
||||
}
|
||||
|
||||
/** Profile content subset used by the prompt builder */
|
||||
export interface ProfileContent {
|
||||
soul?: string | undefined;
|
||||
user?: string | undefined;
|
||||
workspace?: string | undefined;
|
||||
memory?: string | undefined;
|
||||
config?: ProfileConfig | undefined;
|
||||
}
|
||||
|
||||
/** Input options for buildSystemPrompt() */
|
||||
export interface SystemPromptOptions {
|
||||
/** Prompt mode — full for main agents, minimal for subagents, none for bare */
|
||||
mode: SystemPromptMode;
|
||||
/** Agent profile content */
|
||||
profile?: ProfileContent | undefined;
|
||||
/** Profile directory path (so the agent knows where files live) */
|
||||
profileDir?: string | undefined;
|
||||
/** Active tool names (after policy filtering) */
|
||||
tools?: string[] | undefined;
|
||||
/** Skills prompt (pre-built by SkillManager) */
|
||||
skillsPrompt?: string | undefined;
|
||||
/** Runtime context */
|
||||
runtime?: RuntimeInfo | undefined;
|
||||
/** Subagent context (for minimal/none modes) */
|
||||
subagent?: SubagentContext | undefined;
|
||||
/** Extra system prompt to append */
|
||||
extraSystemPrompt?: string | undefined;
|
||||
/** Whether to include the safety constitution (default: true) */
|
||||
includeSafety?: boolean | undefined;
|
||||
}
|
||||
|
||||
/** A named section of the system prompt */
|
||||
export interface PromptSection {
|
||||
/** Section identifier */
|
||||
name: string;
|
||||
/** Section content (joined lines) */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Report entry for a single section */
|
||||
export interface SectionReport {
|
||||
name: string;
|
||||
chars: number;
|
||||
lines: number;
|
||||
included: boolean;
|
||||
}
|
||||
|
||||
/** Telemetry report about a generated system prompt */
|
||||
export interface SystemPromptReport {
|
||||
mode: SystemPromptMode;
|
||||
totalChars: number;
|
||||
totalLines: number;
|
||||
sections: SectionReport[];
|
||||
toolCount: number;
|
||||
skillsIncluded: boolean;
|
||||
safetyIncluded: boolean;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue