@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
299 lines • 13.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LOCAL_SNAPSHOT_FINGERPRINT_VERSION = void 0;
exports.createLocalSnapshot = createLocalSnapshot;
exports.persistLocalSnapshot = persistLocalSnapshot;
exports.serializeLocalSnapshotSpec = serializeLocalSnapshotSpec;
exports.rehydrateLocalSnapshotSpec = rehydrateLocalSnapshotSpec;
exports.replaceLocalSnapshot = replaceLocalSnapshot;
exports.canReuseLocalSnapshotWorkspace = canReuseLocalSnapshotWorkspace;
exports.localSnapshotIsRestorable = localSnapshotIsRestorable;
exports.restoreLocalSnapshotToWorkspace = restoreLocalSnapshotToWorkspace;
exports.computeLocalSnapshotFingerprint = computeLocalSnapshotFingerprint;
exports.copyDirectory = copyDirectory;
exports.isRemoteSnapshotSpec = isRemoteSnapshotSpec;
const errors_1 = require("../../../errors.js");
const snapshot_1 = require("../../snapshot.js");
const promises_1 = require("node:fs/promises");
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const stableJson_1 = require("../../shared/stableJson.js");
const typeGuards_1 = require("../../shared/typeGuards.js");
const snapshotFilesystem_1 = require("./snapshotFilesystem.js");
const workspaceArchive_1 = require("./workspaceArchive.js");
const localSnapshotPaths_1 = require("./localSnapshotPaths.js");
const manifestPersistence_1 = require("./manifestPersistence.js");
exports.LOCAL_SNAPSHOT_FINGERPRINT_VERSION = 'workspace_tree_sha256_v1';
async function createLocalSnapshot(state, spec) {
const snapshotPath = await mkdtempForSnapshot(spec);
await copyDirectory(state.workspaceRootPath, snapshotPath, localSnapshotExcludedPaths(state));
return {
id: (0, node_crypto_1.randomUUID)(),
type: 'local',
path: snapshotPath,
};
}
async function persistLocalSnapshot(providerName, state, spec, capture = (operation) => operation()) {
if ((0, snapshot_1.isNoopSnapshotSpec)(spec)) {
await replaceLocalSnapshot(state, null);
replaceLocalSnapshotFingerprint(state, null);
return null;
}
if (isRemoteSnapshotSpec(spec)) {
const { fingerprint, data } = await capture(async () => ({
fingerprint: await computeLocalSnapshotFingerprint(state),
data: await (0, workspaceArchive_1.createWorkspaceArchive)(state.workspaceRootPath, localSnapshotExcludedPaths(state)),
}));
const saved = await spec.store.save({
id: spec.id,
data,
metadata: spec.metadata,
});
const snapshot = {
id: saved.id,
type: 'remote',
...(saved.metadata ? { metadata: saved.metadata } : {}),
};
await replaceLocalSnapshot(state, snapshot);
replaceLocalSnapshotFingerprint(state, fingerprint);
return snapshot;
}
if (spec && spec.type !== 'local') {
throw new errors_1.UserError(`${providerName} does not support snapshot specs of type "${spec.type}".`);
}
const { fingerprint, snapshot } = await capture(async () => ({
fingerprint: await computeLocalSnapshotFingerprint(state),
snapshot: await createLocalSnapshot(state, spec ?? undefined),
}));
await replaceLocalSnapshot(state, snapshot);
replaceLocalSnapshotFingerprint(state, fingerprint);
return snapshot;
}
function serializeLocalSnapshotSpec(spec) {
if (!spec) {
return null;
}
if (spec.type === 'remote') {
return {
type: 'remote',
...(typeof spec.id === 'string' ? { id: spec.id } : {}),
...(spec.metadata ? { metadata: structuredClone(spec.metadata) } : {}),
};
}
return structuredClone(spec);
}
function rehydrateLocalSnapshotSpec(serialized, configured) {
// Remote SnapshotStore implementations can carry functions or credentials, so
// serialized state stores only lookup fields and reattaches the live configured store.
if (!(0, typeGuards_1.isRecord)(serialized)) {
return null;
}
if (serialized.type !== 'remote') {
return structuredClone(serialized);
}
if (isRemoteSnapshotSpec(serialized)) {
return serialized;
}
if (!isRemoteSnapshotSpec(configured)) {
return null;
}
const id = typeof serialized.id === 'string' ? serialized.id : configured.id;
const metadata = (0, typeGuards_1.isRecord)(serialized.metadata)
? structuredClone(serialized.metadata)
: configured.metadata;
return {
type: 'remote',
store: configured.store,
...(id ? { id } : {}),
...(metadata ? { metadata } : {}),
};
}
async function replaceLocalSnapshot(state, snapshot) {
const previousSnapshot = state.snapshot;
state.snapshot = snapshot;
if (previousSnapshot?.type === 'local' &&
previousSnapshot.path !== snapshot?.path &&
previousSnapshot.path !== state.workspaceRootPath) {
await (0, promises_1.rm)(previousSnapshot.path, { recursive: true, force: true });
}
if (previousSnapshot?.type === 'remote' &&
previousSnapshot.id !== snapshot?.id) {
const store = remoteSnapshotStoreFromSpec(state);
await store?.delete?.({ id: previousSnapshot.id }).catch(() => undefined);
}
}
async function canReuseLocalSnapshotWorkspace(state) {
if (!(await localSnapshotIsRestorable(state))) {
return true;
}
const storedFingerprint = state.snapshotFingerprint;
const storedVersion = state.snapshotFingerprintVersion;
if (!storedFingerprint ||
storedVersion !== exports.LOCAL_SNAPSHOT_FINGERPRINT_VERSION) {
return false;
}
const current = await computeLocalSnapshotFingerprint(state).catch(() => null);
// The fingerprint covers the persisted manifest plus durable workspace tree; ephemeral
// paths are intentionally ignored because they are rebuilt after resume.
return (current?.fingerprint === storedFingerprint &&
current.version === storedVersion);
}
async function localSnapshotIsRestorable(state) {
if (state.snapshot?.type === 'remote') {
const store = remoteSnapshotStoreFromSpec(state);
if (!store) {
return false;
}
if (store.exists) {
return await store.exists({ id: state.snapshot.id });
}
return true;
}
return Boolean(state.snapshot?.type === 'local' && (await pathExists(state.snapshot.path)));
}
async function restoreLocalSnapshotToWorkspace(state, workspaceRootPath, options = {}) {
if (!(await localSnapshotIsRestorable(state))) {
throw new errors_1.UserError('No local snapshot is available to restore.');
}
if (state.snapshot?.type === 'remote') {
const store = remoteSnapshotStoreFromSpec(state);
if (!store) {
throw new errors_1.UserError('No remote snapshot store is available to restore.');
}
const restored = await store.load({ id: state.snapshot.id });
await (0, workspaceArchive_1.restoreWorkspaceArchive)(restored.data, workspaceRootPath, options);
}
else {
await replaceDirectoryContents(state.snapshot.path, workspaceRootPath);
}
state.workspaceRootPath = workspaceRootPath;
const fingerprint = await computeLocalSnapshotFingerprint(state).catch(() => null);
replaceLocalSnapshotFingerprint(state, fingerprint);
return state;
}
async function computeLocalSnapshotFingerprint(state) {
const hash = (0, node_crypto_1.createHash)('sha256');
appendHashFrame(hash, 'version', exports.LOCAL_SNAPSHOT_FINGERPRINT_VERSION);
appendHashFrame(hash, 'manifest', (0, stableJson_1.stableJsonStringify)((0, manifestPersistence_1.serializeManifest)(state.manifest), {
encodeBytes: (value) => ({
type: 'Uint8Array',
data: Buffer.from(value).toString('base64'),
}),
}));
await hashDirectory(hash, state.workspaceRootPath, localSnapshotExcludedPaths(state));
return {
fingerprint: hash.digest('hex'),
version: exports.LOCAL_SNAPSHOT_FINGERPRINT_VERSION,
};
}
function localSnapshotExcludedPaths(state) {
const paths = state.manifest.ephemeralPersistencePaths();
for (const path of state.snapshotExcludedPaths ?? []) {
paths.add(path);
}
return paths;
}
async function copyDirectory(sourceRoot, destinationRoot, excludedLogicalPaths = new Set(), currentRelativePath = '', expectedSourceStats, destinationRootPath = destinationRoot) {
const entries = await (0, snapshotFilesystem_1.readStableSnapshotDirectoryEntries)(sourceRoot, expectedSourceStats);
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRootPath, destinationRoot);
await (0, promises_1.mkdir)(destinationRoot, { recursive: true });
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRootPath, destinationRoot);
for (const entry of entries) {
const relativePath = currentRelativePath
? `${currentRelativePath}/${entry.name}`
: entry.name;
if ((0, snapshotFilesystem_1.shouldSkipSnapshotPath)(relativePath, excludedLogicalPaths)) {
continue;
}
const sourcePath = (0, node_path_1.join)(sourceRoot, entry.name);
const destinationPath = (0, node_path_1.join)(destinationRoot, entry.name);
const sourceStats = await (0, promises_1.lstat)(sourcePath);
if (sourceStats.isDirectory()) {
await copyDirectory(sourcePath, destinationPath, excludedLogicalPaths, relativePath, sourceStats, destinationRootPath);
continue;
}
if (!sourceStats.isFile()) {
continue;
}
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRootPath, (0, node_path_1.dirname)(destinationPath));
await (0, promises_1.mkdir)((0, node_path_1.dirname)(destinationPath), { recursive: true });
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRootPath, (0, node_path_1.dirname)(destinationPath));
await (0, snapshotFilesystem_1.writeStableSnapshotFile)(destinationRootPath, destinationPath, await (0, snapshotFilesystem_1.readStableSnapshotFile)(sourcePath, sourceStats), sourceStats.mode & 0o777);
const destinationStats = await (0, promises_1.lstat)(destinationPath);
if (!destinationStats.isFile()) {
await (0, promises_1.rm)(destinationPath, { recursive: true, force: true });
}
}
}
async function replaceDirectoryContents(sourceRoot, destinationRoot) {
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationRoot);
await (0, promises_1.mkdir)(destinationRoot, { recursive: true });
await (0, snapshotFilesystem_1.assertSafeSnapshotDestinationPath)(destinationRoot, destinationRoot);
await (0, snapshotFilesystem_1.clearDirectory)(destinationRoot);
await copyDirectory(sourceRoot, destinationRoot);
}
function replaceLocalSnapshotFingerprint(state, fingerprint) {
state.snapshotFingerprint = fingerprint?.fingerprint ?? null;
state.snapshotFingerprintVersion = fingerprint?.version ?? null;
}
async function mkdtempForSnapshot(spec) {
const baseDir = spec?.baseDir ?? (0, localSnapshotPaths_1.defaultLocalSnapshotBaseDir)();
await (0, promises_1.mkdir)(baseDir, { recursive: true });
return await (0, promises_1.mkdtemp)((0, node_path_1.join)(baseDir, 'openai-agents-sandbox-snapshot-'));
}
function isRemoteSnapshotSpec(spec) {
const store = spec?.store;
return (spec?.type === 'remote' &&
typeof store === 'object' &&
store !== null &&
typeof store.save === 'function' &&
typeof store.load === 'function');
}
function remoteSnapshotStoreFromSpec(state) {
const spec = state.snapshotSpec;
return isRemoteSnapshotSpec(spec) ? spec.store : undefined;
}
async function pathExists(path) {
try {
await (0, promises_1.stat)(path);
return true;
}
catch {
return false;
}
}
async function hashDirectory(hash, rootPath, excludedLogicalPaths, currentRelativePath = '', expectedSourceStats) {
const entries = await (0, snapshotFilesystem_1.readStableSnapshotDirectoryEntries)(rootPath, expectedSourceStats);
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const relativePath = currentRelativePath
? `${currentRelativePath}/${entry.name}`
: entry.name;
if ((0, snapshotFilesystem_1.shouldSkipSnapshotPath)(relativePath, excludedLogicalPaths)) {
continue;
}
const entryPath = (0, node_path_1.join)(rootPath, entry.name);
const entryStats = await (0, promises_1.lstat)(entryPath);
if (entryStats.isDirectory()) {
appendHashFrame(hash, 'dir', relativePath);
await hashDirectory(hash, entryPath, excludedLogicalPaths, relativePath, entryStats);
continue;
}
if (!entryStats.isFile()) {
continue;
}
const content = await (0, snapshotFilesystem_1.readStableSnapshotFile)(entryPath, entryStats);
appendHashFrame(hash, 'file', relativePath);
appendHashFrame(hash, 'bytes', String(content.byteLength));
hash.update(content);
}
}
function appendHashFrame(hash, label, value) {
// Length-delimited frames avoid collisions such as ["ab", "c"] vs ["a", "bc"].
hash.update(label);
hash.update('\0');
hash.update(String(value.length));
hash.update('\0');
hash.update(value);
hash.update('\0');
}
//# sourceMappingURL=localSnapshots.js.map