mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 23:40:21 +00:00
refactor: localize workspace skill prompt contract
This commit is contained in:
committed by
Peter Steinberger
parent
cc57bcfe2f
commit
9a6dda1b66
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createSyntheticSourceInfo, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import { createSyntheticSourceInfo, type Skill } from "./skills/skill-contract.js";
|
||||
|
||||
export async function writeSkill(params: {
|
||||
dir: string;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import os from "node:os";
|
||||
import { formatSkillsForPrompt, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import { formatSkillsForPrompt as upstreamFormatSkillsForPrompt } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createCanonicalFixtureSkill } from "../skills.test-helpers.js";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skill-contract.js";
|
||||
import type { SkillEntry } from "./types.js";
|
||||
import {
|
||||
formatSkillsCompact,
|
||||
@@ -42,6 +43,16 @@ function buildPrompt(
|
||||
}
|
||||
|
||||
describe("formatSkillsCompact", () => {
|
||||
it("keeps the full-format XML output aligned with the upstream formatter", () => {
|
||||
const hidden: Skill = { ...makeSkill("hidden"), disableModelInvocation: true };
|
||||
const skills = [
|
||||
makeSkill("weather", "Get weather <data> & forecasts"),
|
||||
makeSkill("notes", "Summarize notes", "/tmp/notes/SKILL.md"),
|
||||
hidden,
|
||||
];
|
||||
expect(formatSkillsForPrompt(skills)).toBe(upstreamFormatSkillsForPrompt(skills));
|
||||
});
|
||||
|
||||
it("returns empty string for no skills", () => {
|
||||
expect(formatSkillsCompact([])).toBe("");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Skill } from "@mariozechner/pi-coding-agent";
|
||||
import { validateRegistryNpmSpec } from "../../infra/npm-registry-spec.js";
|
||||
import { parseFrontmatterBlock } from "../../markdown/frontmatter.js";
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
resolveOpenClawManifestOs,
|
||||
resolveOpenClawManifestRequires,
|
||||
} from "../../shared/frontmatter.js";
|
||||
import type { Skill } from "./skill-contract.js";
|
||||
import type {
|
||||
OpenClawSkillMetadata,
|
||||
ParsedSkillFrontmatter,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createSyntheticSourceInfo, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import { openVerifiedFileSync } from "../../infra/safe-open-sync.js";
|
||||
import { parseFrontmatter, resolveSkillInvocationPolicy } from "./frontmatter.js";
|
||||
import { createSyntheticSourceInfo, type Skill } from "./skill-contract.js";
|
||||
|
||||
function isPathWithinRoot(rootRealPath: string, candidatePath: string): boolean {
|
||||
const relative = path.relative(rootRealPath, candidatePath);
|
||||
|
||||
64
src/agents/skills/skill-contract.ts
Normal file
64
src/agents/skills/skill-contract.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Skill as CanonicalSkill, SourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export type SourceScope = "user" | "project" | "temporary";
|
||||
export type SourceOrigin = "package" | "top-level";
|
||||
|
||||
export type Skill = CanonicalSkill & {
|
||||
// Preserve legacy source reads while keeping the canonical upstream shape.
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export function createSyntheticSourceInfo(
|
||||
path: string,
|
||||
options: {
|
||||
source: string;
|
||||
scope?: SourceScope;
|
||||
origin?: SourceOrigin;
|
||||
baseDir?: string;
|
||||
},
|
||||
): SourceInfo {
|
||||
return {
|
||||
path,
|
||||
source: options.source,
|
||||
scope: options.scope ?? "temporary",
|
||||
origin: options.origin ?? "top-level",
|
||||
baseDir: options.baseDir,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this formatter byte-for-byte aligned with the upstream Agent Skills XML
|
||||
* layout so we can avoid importing the full pi-coding-agent package root on the
|
||||
* cold skills path.
|
||||
*/
|
||||
export function formatSkillsForPrompt(skills: Skill[]): string {
|
||||
const visibleSkills = skills.filter((skill) => !skill.disableModelInvocation);
|
||||
if (visibleSkills.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const lines = [
|
||||
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
||||
"Use the read tool to load a skill's file when the task matches its description.",
|
||||
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
||||
"",
|
||||
"<available_skills>",
|
||||
];
|
||||
for (const skill of visibleSkills) {
|
||||
lines.push(" <skill>");
|
||||
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
||||
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
||||
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
||||
lines.push(" </skill>");
|
||||
}
|
||||
lines.push("</available_skills>");
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Skill } from "@mariozechner/pi-coding-agent";
|
||||
import type { Skill } from "./skill-contract.js";
|
||||
|
||||
type SkillSourceCompat = Skill & {
|
||||
sourceInfo?: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Skill } from "@mariozechner/pi-coding-agent";
|
||||
import type { Skill } from "./skill-contract.js";
|
||||
|
||||
export type SkillInstallSpec = {
|
||||
id?: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { formatSkillsForPrompt, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { isPathInside } from "../../infra/path-guards.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
@@ -14,6 +13,7 @@ import { resolveOpenClawMetadata, resolveSkillInvocationPolicy } from "./frontma
|
||||
import { loadSkillsFromDirSafe, readSkillFrontmatterSafe } from "./local-loader.js";
|
||||
import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
||||
import { serializeByKey } from "./serialize.js";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skill-contract.js";
|
||||
import type {
|
||||
ParsedSkillFrontmatter,
|
||||
SkillEligibilityContext,
|
||||
|
||||
Reference in New Issue
Block a user