@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
285 lines • 12.3 kB
JavaScript
import { UserError } from "../../../errors.mjs";
import { isNoopSnapshotSpec, } from "../../snapshot.mjs";
import { lstat, mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
import { createHash, randomUUID } from 'node:crypto';
import { dirname, join } from 'node:path';
import { stableJsonStringify } from "../../shared/stableJson.mjs";
import { isRecord } from "../../shared/typeGuards.mjs";
import { clearDirectory, shouldSkipSnapshotPath, readStableSnapshotDirectoryEntries, readStableSnapshotFile, writeStableSnapshotFile, assertSafeSnapshotDestinationPath, } from "./snapshotFilesystem.mjs";
import { createWorkspaceArchive, restoreWorkspaceArchive, } from "./workspaceArchive.mjs";
import { defaultLocalSnapshotBaseDir } from "./localSnapshotPaths.mjs";
import { serializeManifest } from "./manifestPersistence.mjs";
export const LOCAL_SNAPSHOT_FINGERPRINT_VERSION = 'workspace_tree_sha256_v1';
export async function createLocalSnapshot(state, spec) {
const snapshotPath = await mkdtempForSnapshot(spec);
await copyDirectory(state.workspaceRootPath, snapshotPath, localSnapshotExcludedPaths(state));
return {
id: randomUUID(),
type: 'local',
path: snapshotPath,
};
}
export async function persistLocalSnapshot(providerName, state, spec, capture = (operation) => operation()) {
if (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 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 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;
}
export 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);
}
export 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 (!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 = isRecord(serialized.metadata)
? structuredClone(serialized.metadata)
: configured.metadata;
return {
type: 'remote',
store: configured.store,
...(id ? { id } : {}),
...(metadata ? { metadata } : {}),
};
}
export 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 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);
}
}
export async function canReuseLocalSnapshotWorkspace(state) {
if (!(await localSnapshotIsRestorable(state))) {
return true;
}
const storedFingerprint = state.snapshotFingerprint;
const storedVersion = state.snapshotFingerprintVersion;
if (!storedFingerprint ||
storedVersion !== 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);
}
export 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)));
}
export async function restoreLocalSnapshotToWorkspace(state, workspaceRootPath, options = {}) {
if (!(await localSnapshotIsRestorable(state))) {
throw new UserError('No local snapshot is available to restore.');
}
if (state.snapshot?.type === 'remote') {
const store = remoteSnapshotStoreFromSpec(state);
if (!store) {
throw new UserError('No remote snapshot store is available to restore.');
}
const restored = await store.load({ id: state.snapshot.id });
await 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;
}
export async function computeLocalSnapshotFingerprint(state) {
const hash = createHash('sha256');
appendHashFrame(hash, 'version', LOCAL_SNAPSHOT_FINGERPRINT_VERSION);
appendHashFrame(hash, 'manifest', stableJsonStringify(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: LOCAL_SNAPSHOT_FINGERPRINT_VERSION,
};
}
function localSnapshotExcludedPaths(state) {
const paths = state.manifest.ephemeralPersistencePaths();
for (const path of state.snapshotExcludedPaths ?? []) {
paths.add(path);
}
return paths;
}
export async function copyDirectory(sourceRoot, destinationRoot, excludedLogicalPaths = new Set(), currentRelativePath = '', expectedSourceStats, destinationRootPath = destinationRoot) {
const entries = await readStableSnapshotDirectoryEntries(sourceRoot, expectedSourceStats);
await assertSafeSnapshotDestinationPath(destinationRootPath, destinationRoot);
await mkdir(destinationRoot, { recursive: true });
await assertSafeSnapshotDestinationPath(destinationRootPath, destinationRoot);
for (const entry of entries) {
const relativePath = currentRelativePath
? `${currentRelativePath}/${entry.name}`
: entry.name;
if (shouldSkipSnapshotPath(relativePath, excludedLogicalPaths)) {
continue;
}
const sourcePath = join(sourceRoot, entry.name);
const destinationPath = join(destinationRoot, entry.name);
const sourceStats = await lstat(sourcePath);
if (sourceStats.isDirectory()) {
await copyDirectory(sourcePath, destinationPath, excludedLogicalPaths, relativePath, sourceStats, destinationRootPath);
continue;
}
if (!sourceStats.isFile()) {
continue;
}
await assertSafeSnapshotDestinationPath(destinationRootPath, dirname(destinationPath));
await mkdir(dirname(destinationPath), { recursive: true });
await assertSafeSnapshotDestinationPath(destinationRootPath, dirname(destinationPath));
await writeStableSnapshotFile(destinationRootPath, destinationPath, await readStableSnapshotFile(sourcePath, sourceStats), sourceStats.mode & 0o777);
const destinationStats = await lstat(destinationPath);
if (!destinationStats.isFile()) {
await rm(destinationPath, { recursive: true, force: true });
}
}
}
async function replaceDirectoryContents(sourceRoot, destinationRoot) {
await assertSafeSnapshotDestinationPath(destinationRoot, destinationRoot);
await mkdir(destinationRoot, { recursive: true });
await assertSafeSnapshotDestinationPath(destinationRoot, destinationRoot);
await 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 ?? defaultLocalSnapshotBaseDir();
await mkdir(baseDir, { recursive: true });
return await mkdtemp(join(baseDir, 'openai-agents-sandbox-snapshot-'));
}
export 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 stat(path);
return true;
}
catch {
return false;
}
}
async function hashDirectory(hash, rootPath, excludedLogicalPaths, currentRelativePath = '', expectedSourceStats) {
const entries = await 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 (shouldSkipSnapshotPath(relativePath, excludedLogicalPaths)) {
continue;
}
const entryPath = join(rootPath, entry.name);
const entryStats = await lstat(entryPath);
if (entryStats.isDirectory()) {
appendHashFrame(hash, 'dir', relativePath);
await hashDirectory(hash, entryPath, excludedLogicalPaths, relativePath, entryStats);
continue;
}
if (!entryStats.isFile()) {
continue;
}
const content = await 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.mjs.map