@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
553 lines (549 loc) • 23.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.skills = skills;
exports.parseSkillFrontmatter = parseSkillFrontmatter;
const zod_1 = require("zod");
const tool_1 = require("../../tool.js");
const entries_1 = require("../entries/index.js");
const errors_1 = require("../errors.js");
const manifest_1 = require("../manifest.js");
const pathProbe_1 = require("../shared/pathProbe.js");
const prompts_1 = require("../runtime/prompts.js");
const base_1 = require("./base.js");
class SkillsCapability extends base_1.Capability {
type = 'skills';
skills;
from;
lazyFrom;
index;
skillsPath;
constructor(args) {
super();
this.skills = args.skills ?? [];
this.from = args.from;
this.lazyFrom = args.lazyFrom;
this.index = args.index;
this.skillsPath = (0, manifest_1.normalizeRelativePath)(args.skillsPath ?? '.agents');
validateSkillsCapability(this);
}
tools() {
if (!this.lazyFrom) {
return [];
}
const session = (0, base_1.requireBoundSession)(this.type, this._session);
if (!session.pathExists || !session.materializeEntry) {
throw new errors_1.SandboxSkillsConfigError('Skills sandbox sessions must provide pathExists() and materializeEntry().');
}
return [
(0, tool_1.tool)({
name: 'load_skill',
description: 'Load a single lazily configured skill into the sandbox so its SKILL.md, scripts, references, and assets can be read from the workspace.',
parameters: zod_1.z.object({
skill_name: zod_1.z
.string()
.min(1)
.describe('Name of the lazily configured skill to materialize.'),
}),
execute: async ({ skill_name, }) => {
const match = resolveLazySkillMatch(this, skill_name, session.state.manifest);
const relativeSkillPath = (0, manifest_1.normalizeRelativePath)(match.path ?? match.name);
const destinationPath = joinRelativePaths(this.skillsPath, relativeSkillPath);
const skillMarkdownPath = joinRelativePaths(destinationPath, 'SKILL.md');
if (await session.pathExists(skillMarkdownPath, this._runAs)) {
return {
status: 'already_loaded',
skill_name: match.name,
path: this.modelResourcePath(session.state.manifest, destinationPath),
};
}
await session.materializeEntry({
path: destinationPath,
entry: resolveLazySkillEntry(this.lazyFrom.source, relativeSkillPath),
runAs: this._runAs,
});
return {
status: 'loaded',
skill_name: match.name,
path: this.modelResourcePath(session.state.manifest, destinationPath),
};
},
}),
];
}
processManifest(manifest) {
const existingPaths = new Set(Object.keys(manifest.entries).map((path) => (0, manifest_1.normalizeRelativePath)(path)));
if (this.lazyFrom) {
const overlaps = [...existingPaths]
.filter((path) => pathsOverlap(path, this.skillsPath))
.sort();
if (overlaps.length > 0) {
throw new errors_1.SandboxSkillsConfigError(`skills lazyFrom path overlaps existing manifest entries: ${overlaps.join(', ')}`);
}
return manifest;
}
if (this.from) {
const overlap = [...existingPaths].find((path) => pathsOverlap(path, this.skillsPath));
if (overlap) {
throw new errors_1.SandboxSkillsConfigError(`skills path overlaps existing manifest entries: ${overlap}`);
}
manifest.entries[this.skillsPath] = this.from;
return manifest;
}
for (const skill of this.skills) {
const skillPath = joinRelativePaths(this.skillsPath, skill.name);
const overlap = [...existingPaths].find((path) => pathsOverlap(path, skillPath));
if (overlap) {
throw new errors_1.SandboxSkillsConfigError(`skill path overlaps existing manifest entries: ${skillPath}`);
}
manifest.entries[skillPath] = renderSkillDescriptor(skill);
existingPaths.add(skillPath);
}
return manifest;
}
async instructions(manifest) {
const metadata = resolveSkillsMetadata(this, await this.resolveRuntimeMetadata(manifest), manifest).map((skill) => ({
...skill,
path: this.modelResourcePath(manifest, skill.path ?? skill.name),
}));
if (metadata.length === 0 && this.from) {
return renderSkillsDiscoveryInstructions(this.modelResourcePath(manifest, this.skillsPath));
}
if (metadata.length === 0) {
return null;
}
return renderSkillsInstructions({
metadata,
lazy: Boolean(this.lazyFrom),
});
}
async resolveRuntimeMetadata(_manifest) {
if (!this.from || !this._session?.listDir || !this._session.readFile) {
return [];
}
let entries;
try {
entries = await this._session.listDir({
path: this.skillsPath,
runAs: this._runAs,
});
}
catch (error) {
if ((0, pathProbe_1.isSandboxPathNotFoundError)(error)) {
return [];
}
throw error;
}
const metadata = [];
for (const entry of entries) {
if (entry.type !== 'dir') {
continue;
}
let content;
try {
content = await this._session.readFile({
path: joinRelativePaths(entry.path, 'SKILL.md'),
runAs: this._runAs,
});
}
catch (error) {
if ((0, pathProbe_1.isSandboxPathNotFoundError)(error)) {
continue;
}
throw error;
}
const markdown = typeof content === 'string'
? content
: new TextDecoder().decode(content);
const frontmatter = parseSkillFrontmatter(markdown);
metadata.push({
name: frontmatter.name ?? entry.name,
description: frontmatter.description ?? 'No description provided.',
path: entry.name,
});
}
return metadata;
}
}
function skills(args) {
return new SkillsCapability(args);
}
const SKILLS_SECTION_INTRO = (0, prompts_1.prompt) `
A skill is a set of local instructions to follow that is stored in a \`SKILL.md\` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
`;
const HOW_TO_USE_SKILLS_SECTION = (0, prompts_1.prompt) `
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with \`$SkillName\` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
1) After deciding to use a skill, open its \`SKILL.md\`. Read only enough to follow the workflow.
2) If \`SKILL.md\` points to extra folders such as \`references/\`, load only the specific files needed for the request; don't bulk-load everything.
3) If \`scripts/\` exist, prefer running or patching them instead of retyping large code blocks.
4) If \`assets/\` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
- Avoid deep reference-chasing: prefer opening only files directly linked from \`SKILL.md\` unless you're blocked.
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
`;
const HOW_TO_USE_LAZY_SKILLS_SECTION = (0, prompts_1.prompt) `
### How to use skills
- Discovery: The list above is the skill index available in this session (name + description + workspace path). In lazy mode, those paths are loaded on demand instead of being present up front.
- Trigger rules: If the user names a skill (with \`$SkillName\` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
1) After deciding to use a lazy skill, call \`load_skill\` for that skill first, then open its \`SKILL.md\`.
2) If \`SKILL.md\` points to extra folders such as \`references/\`, load only the specific files needed for the request; don't bulk-load everything.
3) If \`scripts/\` exist, prefer running or patching them instead of retyping large code blocks.
4) If \`assets/\` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
- Avoid deep reference-chasing: prefer opening only files directly linked from \`SKILL.md\` unless you're blocked.
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
`;
function validateSkillsCapability(capability) {
const configuredSources = [
capability.skills.length > 0,
Boolean(capability.from),
Boolean(capability.lazyFrom),
].filter(Boolean).length;
if (configuredSources === 0) {
throw new errors_1.SandboxSkillsConfigError('skills capability requires `skills`, `from`, or `lazyFrom`.');
}
if (configuredSources > 1) {
throw new errors_1.SandboxSkillsConfigError('skills capability accepts only one of `skills`, `from`, or `lazyFrom`.');
}
if (capability.from && !(0, entries_1.isDirectoryLikeEntry)(capability.from)) {
throw new errors_1.SandboxSkillsConfigError('skills from must be a directory-like entry such as dir, git_repo, or mount.');
}
const seenSkillNames = new Set();
for (const skill of capability.skills) {
const normalizedName = (0, manifest_1.normalizeRelativePath)(skill.name);
if (seenSkillNames.has(normalizedName)) {
throw new errors_1.SandboxSkillsConfigError(`duplicate skill name: ${skill.name}`);
}
seenSkillNames.add(normalizedName);
}
}
function renderSkillDescriptor(skill) {
const children = {
'SKILL.md': normalizeSkillContent(skill.content),
};
if (skill.scripts && Object.keys(skill.scripts).length > 0) {
children.scripts = (0, entries_1.dir)({
children: skill.scripts,
});
}
if (skill.references && Object.keys(skill.references).length > 0) {
children.references = (0, entries_1.dir)({
children: skill.references,
});
}
if (skill.assets && Object.keys(skill.assets).length > 0) {
children.assets = (0, entries_1.dir)({
children: skill.assets,
});
}
return (0, entries_1.dir)({
children,
});
}
function normalizeSkillContent(content) {
if (typeof content === 'string' || content instanceof Uint8Array) {
return (0, entries_1.file)({
content,
});
}
return content;
}
function resolveSkillsMetadata(capability, runtimeMetadata = [], manifest) {
if (capability.skills.length > 0) {
return capability.skills
.map((skill) => ({
name: skill.name,
description: skill.description,
path: joinRelativePaths(capability.skillsPath, skill.name),
}))
.sort((left, right) => left.name.localeCompare(right.name));
}
const configuredIndex = resolveLazySkillIndex(capability.lazyFrom, manifest, capability.skillsPath) ??
capability.index ??
(runtimeMetadata.length > 0 ? runtimeMetadata : undefined) ??
deriveIndexFromDirEntry(capability.from);
return configuredIndex
.map((skill) => ({
...skill,
path: joinRelativePaths(capability.skillsPath, skill.path ?? (0, manifest_1.normalizeRelativePath)(skill.name)),
}))
.sort((left, right) => left.name.localeCompare(right.name));
}
/**
* Read string metadata, including wrapped plain values and folded/literal blocks.
* Block indentation/chomping indicators and header comments are accepted, but
* outer block whitespace is trimmed for the skill index regardless of chomping.
* This is not a YAML loader: tags, aliases, collections, multiline quoted values,
* and quoted escapes are not interpreted. Use explicit skill metadata or a lazy
* source index for those formats. Single-line quotes and inline hashes retain
* their existing behavior; nested fields are never promoted to root metadata.
*/
function parseSkillFrontmatter(markdown) {
const lines = markdown.split(/\r?\n/);
if (lines[0]?.trim() !== '---') {
return {};
}
const delimiterIndent = frontmatterIndent(lines[0]);
const endIndex = lines.findIndex((line, index) => index > 0 &&
line.trim() === '---' &&
frontmatterIndent(line) <= delimiterIndent);
if (endIndex === -1) {
return {};
}
const metadata = {};
let rootIndent;
for (let index = 1; index < endIndex;) {
const line = lines[index++];
const stripped = line.trim();
const delimiterIndex = stripped.indexOf(':');
if (!stripped || stripped.startsWith('#') || delimiterIndex === -1) {
continue;
}
const key = stripped.slice(0, delimiterIndex).trim();
const rawValue = stripped.slice(delimiterIndex + 1).trim();
if (!key) {
continue;
}
const keyIndent = frontmatterIndent(line);
rootIndent ??= keyIndent;
if (keyIndent !== rootIndent) {
continue;
}
const blockHeader = rawValue.match(/^([>|])(?:([1-9])[+-]?|[+-]([1-9])?)?(?:[ \t]+#.*)?$/);
const explicitIndent = blockHeader?.[2] ?? blockHeader?.[3];
let contentIndent = explicitIndent
? keyIndent + Number(explicitIndent)
: undefined;
const body = [];
while (index < endIndex) {
const next = lines[index];
// Comments in plain values are metadata comments, not description text.
if (!blockHeader && next.trimStart().startsWith('#')) {
index++;
continue;
}
if (next.trim()) {
const indent = frontmatterIndent(next);
if (indent < (contentIndent ?? keyIndent + 1)) {
break;
}
if (blockHeader) {
contentIndent ??= indent;
}
}
body.push(next);
index++;
}
if (blockHeader) {
const content = body.map((line) => line.slice(contentIndent ?? 0));
metadata[key] = (blockHeader[1] === '|'
? content.join('\n')
: foldFrontmatterLines(content)).trim();
}
else if (/^["']/.test(rawValue)) {
metadata[key] = unquoteFrontmatterValue(rawValue);
}
else {
// Empty values can introduce mappings; consume their children without
// interpreting them as either root fields or a plain string.
metadata[key] = rawValue
? foldFrontmatterLines([rawValue, ...body.map((line) => line.trim())])
: '';
}
}
return metadata;
}
function frontmatterIndent(line) {
// Tabs after the content indentation belong to the scalar itself.
return line.length - line.replace(/^ +/, '').length;
}
function foldFrontmatterLines(lines) {
const parts = [];
let previous = '';
let blankLines = 0;
for (const line of lines) {
if (!line) {
blankLines++;
continue;
}
if (parts.length > 0) {
const moreIndented = /^[ \t]/.test(previous) || /^[ \t]/.test(line);
parts.push(moreIndented
? '\n'.repeat(blankLines + 1)
: '\n'.repeat(blankLines) || ' ');
}
parts.push(line);
previous = line;
blankLines = 0;
}
return parts.join('');
}
function unquoteFrontmatterValue(value) {
if (value.length >= 2 &&
value[0] === value[value.length - 1] &&
(value[0] === '"' || value[0] === "'")) {
return value.slice(1, -1);
}
return value;
}
function resolveLazySkillIndex(lazySource, manifest, skillsPath = '.agents') {
if (!lazySource) {
return undefined;
}
if (lazySource.index) {
return lazySource.index;
}
if (lazySource.getIndex && manifest) {
const derivedIndex = lazySource.getIndex(manifest, skillsPath);
return derivedIndex.length > 0 ? derivedIndex : undefined;
}
const derivedIndex = deriveIndexFromDirEntry(lazySource.source);
return derivedIndex.length > 0 ? derivedIndex : undefined;
}
function deriveIndexFromDirEntry(entry) {
if (!entry || !(0, entries_1.isDir)(entry) || !entry.children) {
return [];
}
return Object.entries(entry.children)
.filter(([, child]) => child.type === 'dir')
.map(([name, child]) => ({
name,
description: child.description ?? 'No description provided.',
}));
}
function renderSkillsDiscoveryInstructions(skillsPath) {
return (0, prompts_1.prompt) `
## Skills
${SKILLS_SECTION_INTRO}
### Skill source
- Skills are materialized under ${skillsPath}. Discover available skills by inspecting child directories and their SKILL.md files at runtime.
${HOW_TO_USE_SKILLS_SECTION}
`;
}
function renderSkillsInstructions(args) {
const availableSkills = args.metadata
.map((skill) => `- ${skill.name}: ${skill.description} (file: ${skill.path ?? skill.name})`)
.join('\n');
const lazyLoadingInstructions = args.lazy
? (0, prompts_1.prompt) `
### Lazy loading
- These skills are indexed for planning, but they are not materialized in the workspace yet.
- Call \`load_skill\` with a single skill name from the list before reading its \`SKILL.md\` or other files from the workspace.
- \`load_skill\` stages exactly one skill under the listed path. If you need more than one skill, call it multiple times.
`
: '';
const usageInstructions = args.lazy
? HOW_TO_USE_LAZY_SKILLS_SECTION
: HOW_TO_USE_SKILLS_SECTION;
const lazyLoadingSection = lazyLoadingInstructions
? `${lazyLoadingInstructions}\n\n`
: '';
return (0, prompts_1.prompt) `
## Skills
${SKILLS_SECTION_INTRO}
### Available skills
${availableSkills}
${lazyLoadingSection}${usageInstructions}
`;
}
function resolveLazySkillMatch(capability, skillName, manifest) {
const index = resolveLazySkillIndex(capability.lazyFrom, manifest, capability.skillsPath) ??
capability.index ??
[];
const matches = index.filter((skill) => {
const relativePath = (0, manifest_1.normalizeRelativePath)(skill.path ?? skill.name);
const pathBaseName = relativePath.split('/').pop() ?? relativePath;
return (skill.name === skillName ||
relativePath === skillName ||
pathBaseName === skillName);
});
if (matches.length === 0) {
throw new errors_1.SandboxSkillsConfigError(`lazy skill not found: ${skillName}`);
}
if (matches.length > 1) {
throw new errors_1.SandboxSkillsConfigError(`lazy skill name is ambiguous: ${skillName}`);
}
return matches[0];
}
function joinRelativePaths(left, right) {
const normalizedLeft = (0, manifest_1.normalizeRelativePath)(left);
const normalizedRight = (0, manifest_1.normalizeRelativePath)(right);
if (!normalizedLeft) {
return normalizedRight;
}
if (!normalizedRight) {
return normalizedLeft;
}
return `${normalizedLeft}/${normalizedRight}`;
}
function joinSourcePath(sourceRoot, relativePath) {
const trimmedRoot = sourceRoot.replace(/[\\/]+$/, '');
if (!trimmedRoot) {
return relativePath;
}
return `${trimmedRoot}/${relativePath}`;
}
function resolveLazySkillEntry(source, relativeSkillPath) {
if (source.type === 'local_dir') {
if (!source.src) {
throw new errors_1.SandboxSkillsConfigError('Lazy skill local_dir sources require a concrete src value.');
}
return {
...source,
type: 'local_dir',
src: joinSourcePath(source.src, relativeSkillPath),
};
}
if (source.type === 'git_repo') {
const baseSubpath = source.subpath
? (0, manifest_1.normalizeRelativePath)(source.subpath)
: '';
return {
...source,
type: 'git_repo',
subpath: joinRelativePaths(baseSubpath, relativeSkillPath),
};
}
if (source.type === 'dir') {
const child = readDirChild(source, relativeSkillPath);
if (!child) {
throw new errors_1.SandboxSkillsConfigError(`lazy skill source path not found: ${relativeSkillPath}`);
}
return child;
}
throw new errors_1.SandboxSkillsConfigError(`Lazy skill source type is not supported yet: ${source.type}`);
}
function readDirChild(source, relativeSkillPath) {
let current = source;
for (const segment of (0, manifest_1.normalizeRelativePath)(relativeSkillPath).split('/')) {
if (!segment) {
continue;
}
if (!current || !(0, entries_1.isDir)(current) || !current.children) {
return undefined;
}
current = current.children[segment];
}
return current;
}
function pathsOverlap(left, right) {
return (left === right ||
left.startsWith(`${right}/`) ||
right.startsWith(`${left}/`));
}
//# sourceMappingURL=skills.js.map