@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
272 lines • 9.09 kB
JavaScript
/**
* Applies a headerless V4A diff to the provided file content.
* - mode "default": patch an existing file using V4A sections ("@@" + +/-/space lines).
* - mode "create": create-file syntax that requires every line to start with "+".
*
* The function preserves trailing newlines from the original file and throws when
* the diff cannot be applied cleanly.
*/
export function applyDiff(input, diff, mode = 'default') {
const diffLines = normalizeDiffLines(diff);
if (mode === 'create') {
return parseCreateDiff(diffLines);
}
const { chunks } = parseUpdateDiff(diffLines, input);
return applyChunks(input, chunks);
}
const END_PATCH = '*** End Patch';
const END_FILE = '*** End of File';
const END_SECTION_MARKERS = [
END_PATCH,
'*** Update File:',
'*** Delete File:',
'*** Add File:',
END_FILE,
];
const SECTION_TERMINATORS = [
END_PATCH,
'*** Update File:',
'*** Delete File:',
'*** Add File:',
];
function normalizeDiffLines(diff) {
return diff
.split(/\r?\n/)
.map((line) => line.replace(/\r$/, ''))
.filter((line, idx, arr) => !(idx === arr.length - 1 && line === ''));
}
function isDone(state, prefixes) {
if (state.index >= state.lines.length)
return true;
if (prefixes.some((p) => state.lines[state.index]?.startsWith(p)))
return true;
return false;
}
function readStr(state, prefix) {
const current = state.lines[state.index];
if (typeof current === 'string' && current.startsWith(prefix)) {
state.index += 1;
return current.slice(prefix.length);
}
return '';
}
function parseCreateDiff(lines) {
const parser = {
lines: [...lines, END_PATCH],
index: 0,
fuzz: 0,
};
const output = [];
while (!isDone(parser, SECTION_TERMINATORS)) {
const line = parser.lines[parser.index];
parser.index += 1;
if (!line.startsWith('+')) {
throw new Error(`Invalid Add File Line: ${line}`);
}
output.push(line.slice(1));
}
return output.join('\n');
}
function parseUpdateDiff(lines, input) {
const parser = {
lines: [...lines, END_PATCH],
index: 0,
fuzz: 0,
};
const inputLines = input.split('\n');
const chunks = [];
let cursor = 0;
while (!isDone(parser, END_SECTION_MARKERS)) {
const anchor = readStr(parser, '@@ ');
const hasBareAnchor = !anchor && parser.lines[parser.index] === '@@';
if (hasBareAnchor)
parser.index += 1;
if (!(anchor || hasBareAnchor || cursor === 0)) {
throw new Error(`Invalid Line:\n${parser.lines[parser.index]}`);
}
if (anchor.trim()) {
cursor = advanceCursorToAnchor(anchor, inputLines, cursor, parser);
}
const { nextContext, sectionChunks, endIndex, eof } = readSection(parser.lines, parser.index);
const nextContextText = nextContext.join('\n');
const { newIndex, fuzz } = findContext(inputLines, nextContext, cursor, eof);
if (newIndex === -1) {
if (eof) {
throw new Error(`Invalid EOF Context ${cursor}:\n${nextContextText}`);
}
throw new Error(`Invalid Context ${cursor}:\n${nextContextText}`);
}
parser.fuzz += fuzz;
for (const ch of sectionChunks) {
chunks.push({ ...ch, origIndex: ch.origIndex + newIndex });
}
cursor = newIndex + nextContext.length;
parser.index = endIndex;
}
return { chunks, fuzz: parser.fuzz };
}
function advanceCursorToAnchor(anchor, inputLines, cursor, parser) {
let found = false;
if (!inputLines.slice(0, cursor).some((s) => s === anchor)) {
for (let i = cursor; i < inputLines.length; i += 1) {
if (inputLines[i] === anchor) {
cursor = i + 1;
found = true;
break;
}
}
}
if (!found &&
!inputLines.slice(0, cursor).some((s) => s.trim() === anchor.trim())) {
for (let i = cursor; i < inputLines.length; i += 1) {
if (inputLines[i].trim() === anchor.trim()) {
cursor = i + 1;
parser.fuzz += 1;
found = true;
break;
}
}
}
return cursor;
}
function readSection(lines, startIndex) {
const context = [];
let delLines = [];
let insLines = [];
const sectionChunks = [];
let mode = 'keep';
let index = startIndex;
const origIndex = index;
while (index < lines.length) {
const raw = lines[index];
if (raw.startsWith('@@') ||
raw.startsWith(END_PATCH) ||
raw.startsWith('*** Update File:') ||
raw.startsWith('*** Delete File:') ||
raw.startsWith('*** Add File:') ||
raw.startsWith(END_FILE)) {
break;
}
if (raw === '***')
break;
if (raw.startsWith('***')) {
throw new Error(`Invalid Line: ${raw}`);
}
index += 1;
const lastMode = mode;
let line = raw;
if (line === '')
line = ' ';
if (line[0] === '+') {
mode = 'add';
}
else if (line[0] === '-') {
mode = 'delete';
}
else if (line[0] === ' ') {
mode = 'keep';
}
else {
throw new Error(`Invalid Line: ${line}`);
}
line = line.slice(1);
const switchingToContext = mode === 'keep' && lastMode !== mode;
if (switchingToContext && (insLines.length || delLines.length)) {
sectionChunks.push({
origIndex: context.length - delLines.length,
delLines,
insLines,
});
delLines = [];
insLines = [];
}
if (mode === 'delete') {
delLines.push(line);
context.push(line);
}
else if (mode === 'add') {
insLines.push(line);
}
else {
context.push(line);
}
}
if (insLines.length || delLines.length) {
sectionChunks.push({
origIndex: context.length - delLines.length,
delLines,
insLines,
});
delLines = [];
insLines = [];
}
if (index < lines.length && lines[index] === END_FILE) {
index += 1;
return { nextContext: context, sectionChunks, endIndex: index, eof: true };
}
if (index === origIndex) {
throw new Error(`Nothing in this section - index=${index} ${lines[index]}`);
}
return { nextContext: context, sectionChunks, endIndex: index, eof: false };
}
function findContext(lines, context, start, eof) {
if (eof) {
const endStart = Math.max(0, lines.length - context.length);
const endMatch = findContextCore(lines, context, endStart);
if (endMatch.newIndex !== -1)
return endMatch;
const fallback = findContextCore(lines, context, start);
return { newIndex: fallback.newIndex, fuzz: fallback.fuzz + 10000 };
}
return findContextCore(lines, context, start);
}
function findContextCore(lines, context, start) {
if (!context.length) {
return { newIndex: start, fuzz: 0 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s))
return { newIndex: i, fuzz: 0 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s.trimEnd()))
return { newIndex: i, fuzz: 1 };
}
for (let i = start; i < lines.length; i += 1) {
if (equalsSlice(lines, context, i, (s) => s.trim()))
return { newIndex: i, fuzz: 100 };
}
return { newIndex: -1, fuzz: 0 };
}
function equalsSlice(source, target, start, mapFn) {
if (start + target.length > source.length)
return false;
for (let i = 0; i < target.length; i += 1) {
if (mapFn(source[start + i]) !== mapFn(target[i]))
return false;
}
return true;
}
function applyChunks(input, chunks) {
const origLines = input.split('\n');
const destLines = [];
let origIndex = 0;
for (const chunk of chunks) {
if (chunk.origIndex > origLines.length) {
throw new Error(`applyDiff: chunk.origIndex ${chunk.origIndex} > input length ${origLines.length}`);
}
if (origIndex > chunk.origIndex) {
throw new Error(`applyDiff: overlapping chunk at ${chunk.origIndex} (cursor ${origIndex})`);
}
destLines.push(...origLines.slice(origIndex, chunk.origIndex));
origIndex = chunk.origIndex;
if (chunk.insLines.length) {
destLines.push(...chunk.insLines);
}
origIndex += chunk.delLines.length;
}
destLines.push(...origLines.slice(origIndex));
const result = destLines.join('\n');
return result;
}
//# sourceMappingURL=applyDiff.mjs.map