UNPKG

@openai/agents-core

Version:

The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.

291 lines • 11 kB
import { UserError } from "../../errors.mjs"; import { dir } from "../entries/index.mjs"; import { normalizeRelativePath } from "../manifest.mjs"; import { withSandboxSpan } from "../runtime/spans.mjs"; import { shellQuote } from "../shared/shell.mjs"; import { renderMemoryReadPrompt } from "../memory/prompts.mjs"; import { joinRelativePaths, SandboxMemoryStorage, } from "../memory/storage.mjs"; import { Capability, requireBoundSession } from "./base.mjs"; const MEMORY_SUMMARY_FILE = 'memory_summary.md'; const MEMORY_SUMMARY_MAX_BYTES = 1_000_000; const DEFAULT_PHASE_ONE_MODEL = 'gpt-5.4-mini'; const DEFAULT_PHASE_TWO_MODEL = 'gpt-5.4'; const DEFAULT_MEMORY_MODEL_SETTINGS = { reasoning: { effort: 'medium' }, }; const MEMORY_BEGIN_MARKER = '__OPENAI_AGENTS_MEMORY_SUMMARY_BEGIN__'; const MEMORY_END_MARKER = '__OPENAI_AGENTS_MEMORY_SUMMARY_END__'; class MemoryCapability extends Capability { type = 'memory'; read; generate; layout; store; constructor(args = {}) { super(); this.read = normalizeReadConfig(args.read); this.generate = normalizeGenerateConfig(args.generate); this.layout = normalizeLayoutConfig(args.layout); this.store = args.store; if (this.read === null && this.generate === null) { throw new UserError('Memory requires at least one of `read` or `generate`.'); } } requiredCapabilityTypes() { if (this.read === null) { return new Set(); } if (this.read.liveUpdate) { return new Set(['filesystem', 'shell']); } return new Set(['shell']); } processManifest(manifest) { if (this.read?.liveUpdate || this.generate !== null) { ensureDirectoryEntry(manifest, this.layout.memoriesDir); } if (this.generate !== null) { ensureDirectoryEntry(manifest, this.layout.sessionsDir); } return manifest; } async instructions(manifest) { if (this.read === null) { return null; } const session = requireBoundSession(this.type, this._session); if (this.store) { await new SandboxMemoryStorage({ session, runAs: this._runAs, store: this.store, tracingParent: this._tracingParent, }).hydrateFromStore(this.layout.memoriesDir); } const memorySummary = await this.readMemorySummary(session); if (!memorySummary) { return null; } return renderMemoryReadPrompt({ memoryDir: this.modelResourcePath(manifest, this.layout.memoriesDir), memorySummary, liveUpdate: Boolean(this.read.liveUpdate), }); } async readMemorySummary(session) { const path = joinRelativePaths(this.layout.memoriesDir, this.layout.summaryFile); if (session.pathExists && !(await session.pathExists(path, this._runAs))) { return await this.readStoredText(path); } if (session.readFile) { try { const payload = await withSandboxSpan('sandbox.memory.read_summary', { path, run_as: this._runAs, }, async () => await session.readFile({ path, runAs: this._runAs, maxBytes: MEMORY_SUMMARY_MAX_BYTES, }), this._tracingParent); return normalizeMemorySummaryPayload(payload); } catch (error) { if (isMissingFileError(error)) { return await this.readStoredText(path); } throw error; } } const stored = await this.readStoredText(path); if (stored !== null) { return stored; } if (session.execCommand) { return await readMemorySummaryWithShell({ session, path, runAs: this._runAs, tracingParent: this._tracingParent, }); } throw new UserError('Memory sandbox sessions must provide readFile() or execCommand().'); } async readStoredText(path) { const stored = await this.store?.read(path); return stored ? normalizeMemorySummaryPayload(stored) : null; } } export function memory(args = {}) { return new MemoryCapability(args); } export function isMemoryCapability(capability) { return capability.type === 'memory' && capability instanceof MemoryCapability; } function normalizeReadConfig(config) { if (config === null || config === false) { return null; } if (config === undefined || config === true) { return { enabled: true, liveUpdate: true }; } rejectKnownSnakeCaseConfigKeys(config, ['live_update'], 'memory read config'); if (config.enabled === false) { return null; } return { enabled: true, liveUpdate: config.liveUpdate ?? true, }; } function normalizeGenerateConfig(config) { if (config === null || config === false) { return null; } if (config === undefined || config === true) { return { enabled: true, maxRawMemoriesForConsolidation: 256, phaseOneModel: DEFAULT_PHASE_ONE_MODEL, phaseOneModelSettings: DEFAULT_MEMORY_MODEL_SETTINGS, phaseTwoModel: DEFAULT_PHASE_TWO_MODEL, phaseTwoModelSettings: DEFAULT_MEMORY_MODEL_SETTINGS, phaseTwoMaxTurns: 500, }; } rejectKnownSnakeCaseConfigKeys(config, [ 'max_raw_memories_for_consolidation', 'phase_one_model', 'phase_one_model_settings', 'phase_two_model', 'phase_two_model_settings', 'phase_two_max_turns', 'extra_prompt', ], 'memory generate config'); if (config.enabled === false) { return null; } const maxRawMemoriesForConsolidation = config.maxRawMemoriesForConsolidation ?? 256; if (!Number.isInteger(maxRawMemoriesForConsolidation) || maxRawMemoriesForConsolidation <= 0 || maxRawMemoriesForConsolidation > 4096) { throw new UserError('MemoryGenerateConfig.maxRawMemoriesForConsolidation must be an integer between 1 and 4096.'); } const sharedModel = normalizeMemoryModel(config.model); const phaseOneModel = config.phaseOneModel ?? sharedModel; const phaseTwoModel = config.phaseTwoModel ?? sharedModel; const extraPrompt = config.extraPrompt ?? config.instructions; const phaseOneModelSettings = config.phaseOneModelSettings ?? DEFAULT_MEMORY_MODEL_SETTINGS; const phaseTwoModelSettings = config.phaseTwoModelSettings ?? DEFAULT_MEMORY_MODEL_SETTINGS; return { enabled: true, maxRawMemoriesForConsolidation, phaseOneModel: phaseOneModel ?? DEFAULT_PHASE_ONE_MODEL, phaseTwoModel: phaseTwoModel ?? DEFAULT_PHASE_TWO_MODEL, phaseOneModelSettings, phaseTwoModelSettings, phaseTwoMaxTurns: config.phaseTwoMaxTurns ?? 500, ...(extraPrompt ? { extraPrompt } : {}), }; } function normalizeMemoryModel(model) { if (typeof model !== 'string') { return model; } const trimmed = model.trim(); return trimmed.length > 0 ? trimmed : undefined; } function normalizeLayoutConfig(config) { if (config !== undefined) { rejectKnownSnakeCaseConfigKeys(config, ['memories_dir', 'sessions_dir', 'summary_file'], 'memory layout config'); } const memoriesDir = normalizeRequiredRelativePath('layout.memoriesDir', config?.memoriesDir ?? config?.directory ?? 'memories'); const sessionsDir = normalizeRequiredRelativePath('layout.sessionsDir', config?.sessionsDir ?? 'sessions'); const summaryFile = normalizeRequiredRelativePath('layout.summaryFile', config?.summaryFile ?? MEMORY_SUMMARY_FILE); if (summaryFile.includes('/')) { throw new UserError('Memory layout.summaryFile must be a file name.'); } return { memoriesDir, sessionsDir, summaryFile, }; } function normalizeRequiredRelativePath(name, value) { let normalized; try { normalized = normalizeRelativePath(value); } catch { throw new UserError(`${name} must be relative to the sandbox workspace root and must not escape root, got: ${value}`); } if (!normalized) { throw new UserError(`${name} must be non-empty.`); } return normalized; } function ensureDirectoryEntry(manifest, path) { const normalizedPath = normalizeRelativePath(path); if (!normalizedPath || manifestHasOverlappingEntry(manifest, normalizedPath)) { return; } manifest.entries[normalizedPath] = dir(); } function manifestHasOverlappingEntry(manifest, path) { const prefix = `${path}/`; for (const entryPath of Object.keys(manifest.entries)) { const normalizedEntryPath = normalizeRelativePath(entryPath); if (normalizedEntryPath === path || normalizedEntryPath.startsWith(prefix) || path.startsWith(`${normalizedEntryPath}/`)) { return true; } } return false; } async function readMemorySummaryWithShell(args) { const command = [ `if [ -f ${shellQuote(args.path)} ]; then`, `printf '%s\n' ${shellQuote(MEMORY_BEGIN_MARKER)};`, `head -c ${MEMORY_SUMMARY_MAX_BYTES} ${shellQuote(args.path)};`, `printf '\n%s\n' ${shellQuote(MEMORY_END_MARKER)};`, 'fi', ].join(' '); const output = await withSandboxSpan('sandbox.memory.read_summary', { path: args.path, run_as: args.runAs, }, async () => await args.session.execCommand({ cmd: command, login: false, yieldTimeMs: 10_000, maxOutputTokens: 20_000, runAs: args.runAs, }), args.tracingParent); const begin = output.indexOf(MEMORY_BEGIN_MARKER); const end = output.indexOf(MEMORY_END_MARKER, begin + MEMORY_BEGIN_MARKER.length); if (begin < 0 || end < 0) { return null; } return normalizeMemorySummaryPayload(output.slice(begin + MEMORY_BEGIN_MARKER.length, end)); } function normalizeMemorySummaryPayload(payload) { const text = typeof payload === 'string' ? payload : new TextDecoder('utf-8', { fatal: false }).decode(payload); const normalized = text.trim(); return normalized.length > 0 ? normalized : null; } function isMissingFileError(error) { return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'); } function rejectKnownSnakeCaseConfigKeys(value, keys, source) { const found = keys.find((key) => key in value); if (!found) { return; } throw new UserError(`Use camelCase config keys in ${source}; snake_case key "${found}" is not supported.`); } //# sourceMappingURL=memory.mjs.map