UNPKG

@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
"use strict"; 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